/*!
 * Copyright 2015 Drifty Co.
 * http://drifty.com/
 *
 * Ionic, v1.3.2
 * A powerful HTML5 mobile app framework.
 * http://ionicframework.com/
 *
 * By @maxlynch, @benjsperry, @adamdbradley <3
 *
 * Licensed under the MIT license. Please see LICENSE for more information.
 *
 */

(function () {

// Create global ionic obj and its namespaces
// build processes may have already created an ionic obj
    window.ionic = window.ionic || {};
    window.ionic.views = {};
    window.ionic.version = '1.3.2';

    (function (ionic) {

        ionic.DelegateService = function (methodNames) {

            if (methodNames.indexOf('$getByHandle') > -1) {
                throw new Error("Method '$getByHandle' is implicitly added to each delegate service. Do not list it as a method.");
            }

            function trueFn() {
                return true;
            }

            return ['$log', function ($log) {

                /*
                 * Creates a new object that will have all the methodNames given,
                 * and call them on the given the controller instance matching given
                 * handle.
                 * The reason we don't just let $getByHandle return the controller instance
                 * itself is that the controller instance might not exist yet.
                 *
                 * We want people to be able to do
                 * `var instance = $ionicScrollDelegate.$getByHandle('foo')` on controller
                 * instantiation, but on controller instantiation a child directive
                 * may not have been compiled yet!
                 *
                 * So this is our way of solving this problem: we create an object
                 * that will only try to fetch the controller with given handle
                 * once the methods are actually called.
                 */
                function DelegateInstance(instances, handle) {
                    this._instances = instances;
                    this.handle = handle;
                }

                methodNames.forEach(function (methodName) {
                    DelegateInstance.prototype[methodName] = instanceMethodCaller(methodName);
                });


                /**
                 * The delegate service (eg $ionicNavBarDelegate) is just an instance
                 * with a non-defined handle, a couple extra methods for registering
                 * and narrowing down to a specific handle.
                 */
                function DelegateService() {
                    this._instances = [];
                }

                DelegateService.prototype = DelegateInstance.prototype;
                DelegateService.prototype._registerInstance = function (instance, handle, filterFn) {
                    var instances = this._instances;
                    instance.$$delegateHandle = handle;
                    instance.$$filterFn = filterFn || trueFn;
                    instances.push(instance);

                    return function deregister() {
                        var index = instances.indexOf(instance);
                        if (index !== -1) {
                            instances.splice(index, 1);
                        }
                    };
                };
                DelegateService.prototype.$getByHandle = function (handle) {
                    return new DelegateInstance(this._instances, handle);
                };

                return new DelegateService();

                function instanceMethodCaller(methodName) {
                    return function caller() {
                        var handle = this.handle;
                        var args = arguments;
                        var foundInstancesCount = 0;
                        var returnValue;

                        this._instances.forEach(function (instance) {
                            if ((!handle || handle == instance.$$delegateHandle) && instance.$$filterFn(instance)) {
                                foundInstancesCount++;
                                var ret = instance[methodName].apply(instance, args);
                                //Only return the value from the first call
                                if (foundInstancesCount === 1) {
                                    returnValue = ret;
                                }
                            }
                        });

                        if (!foundInstancesCount && handle) {
                            return $log.warn(
                                'Delegate for handle "' + handle + '" could not find a ' +
                                'corresponding element with delegate-handle="' + handle + '"! ' +
                                methodName + '() was not called!\n' +
                                'Possible cause: If you are calling ' + methodName + '() immediately, and ' +
                                'your element with delegate-handle="' + handle + '" is a child of your ' +
                                'controller, then your element may not be compiled yet. Put a $timeout ' +
                                'around your call to ' + methodName + '() and try again.'
                            );
                        }
                        return returnValue;
                    };
                }

            }];
        };

    })(window.ionic);

    (function (window, document, ionic) {

        var readyCallbacks = [];
        var isDomReady = document.readyState === 'complete' || document.readyState === 'interactive';

        function domReady() {
            isDomReady = true;
            for (var x = 0; x < readyCallbacks.length; x++) {
                ionic.requestAnimationFrame(readyCallbacks[x]);
            }
            readyCallbacks = [];
            document.removeEventListener('DOMContentLoaded', domReady);
        }

        if (!isDomReady) {
            document.addEventListener('DOMContentLoaded', domReady);
        }


        // From the man himself, Mr. Paul Irish.
        // The requestAnimationFrame polyfill
        // Put it on window just to preserve its context
        // without having to use .call
        window._rAF = (function () {
            return window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                function (callback) {
                    window.setTimeout(callback, 16);
                };
        })();

        var cancelAnimationFrame = window.cancelAnimationFrame ||
            window.webkitCancelAnimationFrame ||
            window.mozCancelAnimationFrame ||
            window.webkitCancelRequestAnimationFrame;

        /**
         * @ngdoc utility
         * @name ionic.DomUtil
         * @module ionic
         */
        ionic.DomUtil = {
            //Call with proper context
            /**
             * @ngdoc method
             * @name ionic.DomUtil#requestAnimationFrame
             * @alias ionic.requestAnimationFrame
             * @description Calls [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame), or a polyfill if not available.
             * @param {function} callback The function to call when the next frame
             * happens.
             */
            requestAnimationFrame: function (cb) {
                return window._rAF(cb);
            },

            cancelAnimationFrame: function (requestId) {
                cancelAnimationFrame(requestId);
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#animationFrameThrottle
             * @alias ionic.animationFrameThrottle
             * @description
             * When given a callback, if that callback is called 100 times between
             * animation frames, adding Throttle will make it only run the last of
             * the 100 calls.
             *
             * @param {function} callback a function which will be throttled to
             * requestAnimationFrame
             * @returns {function} A function which will then call the passed in callback.
             * The passed in callback will receive the context the returned function is
             * called with.
             */
            animationFrameThrottle: function (cb) {
                var args, isQueued, context;
                return function () {
                    args = arguments;
                    context = this;
                    if (!isQueued) {
                        isQueued = true;
                        ionic.requestAnimationFrame(function () {
                            cb.apply(context, args);
                            isQueued = false;
                        });
                    }
                };
            },

            contains: function (parentNode, otherNode) {
                var current = otherNode;
                while (current) {
                    if (current === parentNode) return true;
                    current = current.parentNode;
                }
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#getPositionInParent
             * @description
             * Find an element's scroll offset within its container.
             * @param {DOMElement} element The element to find the offset of.
             * @returns {object} A position object with the following properties:
             *   - `{number}` `left` The left offset of the element.
             *   - `{number}` `top` The top offset of the element.
             */
            getPositionInParent: function (el) {
                return {
                    left: el.offsetLeft,
                    top: el.offsetTop
                };
            },

            getOffsetTop: function (el) {
                var curtop = 0;
                if (el.offsetParent) {
                    do {
                        curtop += el.offsetTop;
                        el = el.offsetParent;
                    } while (el)
                    return curtop;
                }
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#ready
             * @description
             * Call a function when the DOM is ready, or if it is already ready
             * call the function immediately.
             * @param {function} callback The function to be called.
             */
            ready: function (cb) {
                if (isDomReady) {
                    ionic.requestAnimationFrame(cb);
                } else {
                    readyCallbacks.push(cb);
                }
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#getTextBounds
             * @description
             * Get a rect representing the bounds of the given textNode.
             * @param {DOMElement} textNode The textNode to find the bounds of.
             * @returns {object} An object representing the bounds of the node. Properties:
             *   - `{number}` `left` The left position of the textNode.
             *   - `{number}` `right` The right position of the textNode.
             *   - `{number}` `top` The top position of the textNode.
             *   - `{number}` `bottom` The bottom position of the textNode.
             *   - `{number}` `width` The width of the textNode.
             *   - `{number}` `height` The height of the textNode.
             */
            getTextBounds: function (textNode) {
                if (document.createRange) {
                    var range = document.createRange();
                    range.selectNodeContents(textNode);
                    if (range.getBoundingClientRect) {
                        var rect = range.getBoundingClientRect();
                        if (rect) {
                            var sx = window.scrollX;
                            var sy = window.scrollY;

                            return {
                                top: rect.top + sy,
                                left: rect.left + sx,
                                right: rect.left + sx + rect.width,
                                bottom: rect.top + sy + rect.height,
                                width: rect.width,
                                height: rect.height
                            };
                        }
                    }
                }
                return null;
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#getChildIndex
             * @description
             * Get the first index of a child node within the given element of the
             * specified type.
             * @param {DOMElement} element The element to find the index of.
             * @param {string} type The nodeName to match children of element against.
             * @returns {number} The index, or -1, of a child with nodeName matching type.
             */
            getChildIndex: function (element, type) {
                if (type) {
                    var ch = element.parentNode.children;
                    var c;
                    for (var i = 0, k = 0, j = ch.length; i < j; i++) {
                        c = ch[i];
                        if (c.nodeName && c.nodeName.toLowerCase() == type) {
                            if (c == element) {
                                return k;
                            }
                            k++;
                        }
                    }
                }
                return Array.prototype.slice.call(element.parentNode.children).indexOf(element);
            },

            /**
             * @private
             */
            swapNodes: function (src, dest) {
                dest.parentNode.insertBefore(src, dest);
            },

            elementIsDescendant: function (el, parent, stopAt) {
                var current = el;
                do {
                    if (current === parent) return true;
                    current = current.parentNode;
                } while (current && current !== stopAt);
                return false;
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#getParentWithClass
             * @param {DOMElement} element
             * @param {string} className
             * @returns {DOMElement} The closest parent of element matching the
             * className, or null.
             */
            getParentWithClass: function (e, className, depth) {
                depth = depth || 10;
                while (e.parentNode && depth--) {
                    if (e.parentNode.classList && e.parentNode.classList.contains(className)) {
                        return e.parentNode;
                    }
                    e = e.parentNode;
                }
                return null;
            },
            /**
             * @ngdoc method
             * @name ionic.DomUtil#getParentOrSelfWithClass
             * @param {DOMElement} element
             * @param {string} className
             * @returns {DOMElement} The closest parent or self matching the
             * className, or null.
             */
            getParentOrSelfWithClass: function (e, className, depth) {
                depth = depth || 10;
                while (e && depth--) {
                    if (e.classList && e.classList.contains(className)) {
                        return e;
                    }
                    e = e.parentNode;
                }
                return null;
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#rectContains
             * @param {number} x
             * @param {number} y
             * @param {number} x1
             * @param {number} y1
             * @param {number} x2
             * @param {number} y2
             * @returns {boolean} Whether {x,y} fits within the rectangle defined by
             * {x1,y1,x2,y2}.
             */
            rectContains: function (x, y, x1, y1, x2, y2) {
                if (x < x1 || x > x2) return false;
                if (y < y1 || y > y2) return false;
                return true;
            },

            /**
             * @ngdoc method
             * @name ionic.DomUtil#blurAll
             * @description
             * Blurs any currently focused input element
             * @returns {DOMElement} The element blurred or null
             */
            blurAll: function () {
                if (document.activeElement && document.activeElement != document.body) {
                    document.activeElement.blur();
                    return document.activeElement;
                }
                return null;
            },

            cachedAttr: function (ele, key, value) {
                ele = ele && ele.length && ele[0] || ele;
                if (ele && ele.setAttribute) {
                    var dataKey = '$attr-' + key;
                    if (arguments.length > 2) {
                        if (ele[dataKey] !== value) {
                            ele.setAttribute(key, value);
                            ele[dataKey] = value;
                        }
                    } else if (typeof ele[dataKey] == 'undefined') {
                        ele[dataKey] = ele.getAttribute(key);
                    }
                    return ele[dataKey];
                }
            },

            cachedStyles: function (ele, styles) {
                ele = ele && ele.length && ele[0] || ele;
                if (ele && ele.style) {
                    for (var prop in styles) {
                        if (ele['$style-' + prop] !== styles[prop]) {
                            ele.style[prop] = ele['$style-' + prop] = styles[prop];
                        }
                    }
                }
            }

        };

        //Shortcuts
        ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame;
        ionic.cancelAnimationFrame = ionic.DomUtil.cancelAnimationFrame;
        ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle;

    })(window, document, ionic);

    /**
     * ion-events.js
     *
     * Author: Max Lynch <max@drifty.com>
     *
     * Framework events handles various mobile browser events, and
     * detects special events like tap/swipe/etc. and emits them
     * as custom events that can be used in an app.
     *
     * Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys!
     */

    (function (ionic) {

        // Custom event polyfill
        ionic.CustomEvent = (function () {
            if (typeof window.CustomEvent === 'function') return CustomEvent;

            var customEvent = function (event, params) {
                var evt;
                params = params || {
                        bubbles: false,
                        cancelable: false,
                        detail: undefined
                    };
                try {
                    evt = document.createEvent("CustomEvent");
                    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
                } catch (error) {
                    // fallback for browsers that don't support createEvent('CustomEvent')
                    evt = document.createEvent("Event");
                    for (var param in params) {
                        evt[param] = params[param];
                    }
                    evt.initEvent(event, params.bubbles, params.cancelable);
                }
                return evt;
            };
            customEvent.prototype = window.Event.prototype;
            return customEvent;
        })();


        /**
         * @ngdoc utility
         * @name ionic.EventController
         * @module ionic
         */
        ionic.EventController = {
            VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],

            /**
             * @ngdoc method
             * @name ionic.EventController#trigger
             * @alias ionic.trigger
             * @param {string} eventType The event to trigger.
             * @param {object} data The data for the event. Hint: pass in
             * `{target: targetElement}`
             * @param {boolean=} bubbles Whether the event should bubble up the DOM.
             * @param {boolean=} cancelable Whether the event should be cancelable.
             */
            // Trigger a new event
            trigger: function (eventType, data, bubbles, cancelable) {
                var event = new ionic.CustomEvent(eventType, {
                    detail: data,
                    bubbles: !!bubbles,
                    cancelable: !!cancelable
                });

                // Make sure to trigger the event on the given target, or dispatch it from
                // the window if we don't have an event target
                data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
            },

            /**
             * @ngdoc method
             * @name ionic.EventController#on
             * @alias ionic.on
             * @description Listen to an event on an element.
             * @param {string} type The event to listen for.
             * @param {function} callback The listener to be called.
             * @param {DOMElement} element The element to listen for the event on.
             */
            on: function (type, callback, element) {
                var e = element || window;

                // Bind a gesture if it's a virtual event
                for (var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) {
                    if (type == this.VIRTUALIZED_EVENTS[i]) {
                        var gesture = new ionic.Gesture(element);
                        gesture.on(type, callback);
                        return gesture;
                    }
                }

                // Otherwise bind a normal event
                e.addEventListener(type, callback);
            },

            /**
             * @ngdoc method
             * @name ionic.EventController#off
             * @alias ionic.off
             * @description Remove an event listener.
             * @param {string} type
             * @param {function} callback
             * @param {DOMElement} element
             */
            off: function (type, callback, element) {
                element.removeEventListener(type, callback);
            },

            /**
             * @ngdoc method
             * @name ionic.EventController#onGesture
             * @alias ionic.onGesture
             * @description Add an event listener for a gesture on an element.
             *
             * Available eventTypes (from [hammer.js](http://eightmedia.github.io/hammer.js/)):
             *
             * `hold`, `tap`, `doubletap`, `drag`, `dragstart`, `dragend`, `dragup`, `dragdown`, <br/>
             * `dragleft`, `dragright`, `swipe`, `swipeup`, `swipedown`, `swipeleft`, `swiperight`, <br/>
             * `transform`, `transformstart`, `transformend`, `rotate`, `pinch`, `pinchin`, `pinchout`, <br/>
             * `touch`, `release`
             *
             * @param {string} eventType The gesture event to listen for.
             * @param {function(e)} callback The function to call when the gesture
             * happens.
             * @param {DOMElement} element The angular element to listen for the event on.
             * @param {object} options object.
             * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
             */
            onGesture: function (type, callback, element, options) {
                var gesture = new ionic.Gesture(element, options);
                gesture.on(type, callback);
                return gesture;
            },

            /**
             * @ngdoc method
             * @name ionic.EventController#offGesture
             * @alias ionic.offGesture
             * @description Remove an event listener for a gesture created on an element.
             * @param {ionic.Gesture} gesture The gesture that should be removed.
             * @param {string} eventType The gesture event to remove the listener for.
             * @param {function(e)} callback The listener to remove.

             */
            offGesture: function (gesture, type, callback) {
                gesture && gesture.off(type, callback);
            },

            handlePopState: function () {
            }
        };


        // Map some convenient top-level functions for event handling
        ionic.on = function () {
            ionic.EventController.on.apply(ionic.EventController, arguments);
        };
        ionic.off = function () {
            ionic.EventController.off.apply(ionic.EventController, arguments);
        };
        ionic.trigger = ionic.EventController.trigger;//function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); };
        ionic.onGesture = function () {
            return ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments);
        };
        ionic.offGesture = function () {
            return ionic.EventController.offGesture.apply(ionic.EventController.offGesture, arguments);
        };

    })(window.ionic);

    /* eslint camelcase:0 */
    /**
     * Simple gesture controllers with some common gestures that emit
     * gesture events.
     *
     * Ported from github.com/EightMedia/hammer.js Gestures - thanks!
     */
    (function (ionic) {

        /**
         * ionic.Gestures
         * use this to create instances
         * @param   {HTMLElement}   element
         * @param   {Object}        options
         * @returns {ionic.Gestures.Instance}
         * @constructor
         */
        ionic.Gesture = function (element, options) {
            return new ionic.Gestures.Instance(element, options || {});
        };

        ionic.Gestures = {};

        // default settings
        ionic.Gestures.defaults = {
            // add css to the element to prevent the browser from doing
            // its native behavior. this doesnt prevent the scrolling,
            // but cancels the contextmenu, tap highlighting etc
            // set to false to disable this
            stop_browser_behavior: 'disable-user-behavior'
        };

        // detect touchevents
        ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
        ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window);

        // dont use mouseevents on mobile devices
        ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
        ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX);

        // eventtypes per touchevent (start, move, end)
        // are filled by ionic.Gestures.event.determineEventTypes on setup
        ionic.Gestures.EVENT_TYPES = {};

        // direction defines
        ionic.Gestures.DIRECTION_DOWN = 'down';
        ionic.Gestures.DIRECTION_LEFT = 'left';
        ionic.Gestures.DIRECTION_UP = 'up';
        ionic.Gestures.DIRECTION_RIGHT = 'right';

        // pointer type
        ionic.Gestures.POINTER_MOUSE = 'mouse';
        ionic.Gestures.POINTER_TOUCH = 'touch';
        ionic.Gestures.POINTER_PEN = 'pen';

        // touch event defines
        ionic.Gestures.EVENT_START = 'start';
        ionic.Gestures.EVENT_MOVE = 'move';
        ionic.Gestures.EVENT_END = 'end';

        // hammer document where the base events are added at
        ionic.Gestures.DOCUMENT = window.document;

        // plugins namespace
        ionic.Gestures.plugins = {};

        // if the window events are set...
        ionic.Gestures.READY = false;

        /**
         * setup events to detect gestures on the document
         */
        function setup() {
            if (ionic.Gestures.READY) {
                return;
            }

            // find what eventtypes we add listeners to
            ionic.Gestures.event.determineEventTypes();

            // Register all gestures inside ionic.Gestures.gestures
            for (var name in ionic.Gestures.gestures) {
                if (ionic.Gestures.gestures.hasOwnProperty(name)) {
                    ionic.Gestures.detection.register(ionic.Gestures.gestures[name]);
                }
            }

            // Add touch events on the document
            ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect);
            ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect);

            // ionic.Gestures is ready...!
            ionic.Gestures.READY = true;
        }

        /**
         * create new hammer instance
         * all methods should return the instance itself, so it is chainable.
         * @param   {HTMLElement}       element
         * @param   {Object}            [options={}]
         * @returns {ionic.Gestures.Instance}
         * @name Gesture.Instance
         * @constructor
         */
        ionic.Gestures.Instance = function (element, options) {
            var self = this;

            // A null element was passed into the instance, which means
            // whatever lookup was done to find this element failed to find it
            // so we can't listen for events on it.
            if (element === null) {
                void 0;
                return this;
            }

            // setup ionic.GesturesJS window events and register all gestures
            // this also sets up the default options
            setup();

            this.element = element;

            // start/stop detection option
            this.enabled = true;

            // merge options
            this.options = ionic.Gestures.utils.extend(
                ionic.Gestures.utils.extend({}, ionic.Gestures.defaults),
                options || {});

            // add some css to the element to prevent the browser from doing its native behavoir
            if (this.options.stop_browser_behavior) {
                ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
            }

            // start detection on touchstart
            ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function (ev) {
                if (self.enabled) {
                    ionic.Gestures.detection.startDetect(self, ev);
                }
            });

            // return instance
            return this;
        };


        ionic.Gestures.Instance.prototype = {
            /**
             * bind events to the instance
             * @param   {String}      gesture
             * @param   {Function}    handler
             * @returns {ionic.Gestures.Instance}
             */
            on: function onEvent(gesture, handler) {
                var gestures = gesture.split(' ');
                for (var t = 0; t < gestures.length; t++) {
                    this.element.addEventListener(gestures[t], handler, false);
                }
                return this;
            },


            /**
             * unbind events to the instance
             * @param   {String}      gesture
             * @param   {Function}    handler
             * @returns {ionic.Gestures.Instance}
             */
            off: function offEvent(gesture, handler) {
                var gestures = gesture.split(' ');
                for (var t = 0; t < gestures.length; t++) {
                    this.element.removeEventListener(gestures[t], handler, false);
                }
                return this;
            },


            /**
             * trigger gesture event
             * @param   {String}      gesture
             * @param   {Object}      eventData
             * @returns {ionic.Gestures.Instance}
             */
            trigger: function triggerEvent(gesture, eventData) {
                // create DOM event
                var event = ionic.Gestures.DOCUMENT.createEvent('Event');
                event.initEvent(gesture, true, true);
                event.gesture = eventData;

                // trigger on the target if it is in the instance element,
                // this is for event delegation tricks
                var element = this.element;
                if (ionic.Gestures.utils.hasParent(eventData.target, element)) {
                    element = eventData.target;
                }

                element.dispatchEvent(event);
                return this;
            },


            /**
             * enable of disable hammer.js detection
             * @param   {Boolean}   state
             * @returns {ionic.Gestures.Instance}
             */
            enable: function enable(state) {
                this.enabled = state;
                return this;
            }
        };

        /**
         * this holds the last move event,
         * used to fix empty touchend issue
         * see the onTouch event for an explanation
         * type {Object}
         */
        var last_move_event = null;


        /**
         * when the mouse is hold down, this is true
         * type {Boolean}
         */
        var enable_detect = false;


        /**
         * when touch events have been fired, this is true
         * type {Boolean}
         */
        var touch_triggered = false;


        ionic.Gestures.event = {
            /**
             * simple addEventListener
             * @param   {HTMLElement}   element
             * @param   {String}        type
             * @param   {Function}      handler
             */
            bindDom: function (element, type, handler) {
                var types = type.split(' ');
                for (var t = 0; t < types.length; t++) {
                    element.addEventListener(types[t], handler, false);
                }
            },


            /**
             * touch events with mouse fallback
             * @param   {HTMLElement}   element
             * @param   {String}        eventType        like ionic.Gestures.EVENT_MOVE
             * @param   {Function}      handler
             */
            onTouch: function onTouch(element, eventType, handler) {
                var self = this;

                this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
                    var sourceEventType = ev.type.toLowerCase();

                    // onmouseup, but when touchend has been fired we do nothing.
                    // this is for touchdevices which also fire a mouseup on touchend
                    if (sourceEventType.match(/mouse/) && touch_triggered) {
                        return;
                    }

                    // mousebutton must be down or a touch event
                    else if (sourceEventType.match(/touch/) ||   // touch events are always on screen
                        sourceEventType.match(/pointerdown/) || // pointerevents touch
                        (sourceEventType.match(/mouse/) && ev.which === 1)   // mouse is pressed
                    ) {
                        enable_detect = true;
                    }

                    // mouse isn't pressed
                    else if (sourceEventType.match(/mouse/) && ev.which !== 1) {
                        enable_detect = false;
                    }


                    // we are in a touch event, set the touch triggered bool to true,
                    // this for the conflicts that may occur on ios and android
                    if (sourceEventType.match(/touch|pointer/)) {
                        touch_triggered = true;
                    }

                    // count the total touches on the screen
                    var count_touches = 0;

                    // when touch has been triggered in this detection session
                    // and we are now handling a mouse event, we stop that to prevent conflicts
                    if (enable_detect) {
                        // update pointerevent
                        if (ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) {
                            count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
                        }
                        // touch
                        else if (sourceEventType.match(/touch/)) {
                            count_touches = ev.touches.length;
                        }
                        // mouse
                        else if (!touch_triggered) {
                            count_touches = sourceEventType.match(/up/) ? 0 : 1;
                        }

                        // if we are in a end event, but when we remove one touch and
                        // we still have enough, set eventType to move
                        if (count_touches > 0 && eventType == ionic.Gestures.EVENT_END) {
                            eventType = ionic.Gestures.EVENT_MOVE;
                        }
                        // no touches, force the end event
                        else if (!count_touches) {
                            eventType = ionic.Gestures.EVENT_END;
                        }

                        // store the last move event
                        if (count_touches || last_move_event === null) {
                            last_move_event = ev;
                        }

                        // trigger the handler
                        handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev));

                        // remove pointerevent from list
                        if (ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) {
                            count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
                        }
                    }

                    //debug(sourceEventType +" "+ eventType);

                    // on the end we reset everything
                    if (!count_touches) {
                        last_move_event = null;
                        enable_detect = false;
                        touch_triggered = false;
                        ionic.Gestures.PointerEvent.reset();
                    }
                });
            },


            /**
             * we have different events for each device/browser
             * determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant
             */
            determineEventTypes: function determineEventTypes() {
                // determine the eventtype we want to set
                var types;

                // pointerEvents magic
                if (ionic.Gestures.HAS_POINTEREVENTS) {
                    types = ionic.Gestures.PointerEvent.getEvents();
                }
                // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
                else if (ionic.Gestures.NO_MOUSEEVENTS) {
                    types = [
                        'touchstart',
                        'touchmove',
                        'touchend touchcancel'];
                }
                // for non pointer events browsers and mixed browsers,
                // like chrome on windows8 touch laptop
                else {
                    types = [
                        'touchstart mousedown',
                        'touchmove mousemove',
                        'touchend touchcancel mouseup'];
                }

                ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START] = types[0];
                ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE] = types[1];
                ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END] = types[2];
            },


            /**
             * create touchlist depending on the event
             * @param   {Object}    ev
             * @param   {String}    eventType   used by the fakemultitouch plugin
             */
            getTouchList: function getTouchList(ev/*, eventType*/) {
                // get the fake pointerEvent touchlist
                if (ionic.Gestures.HAS_POINTEREVENTS) {
                    return ionic.Gestures.PointerEvent.getTouchList();
                }
                // get the touchlist
                else if (ev.touches) {
                    return ev.touches;
                }
                // make fake touchlist from mouse position
                else {
                    ev.identifier = 1;
                    return [ev];
                }
            },


            /**
             * collect event data for ionic.Gestures js
             * @param   {HTMLElement}   element
             * @param   {String}        eventType        like ionic.Gestures.EVENT_MOVE
             * @param   {Object}        eventData
             */
            collectEventData: function collectEventData(element, eventType, touches, ev) {

                // find out pointerType
                var pointerType = ionic.Gestures.POINTER_TOUCH;
                if (ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) {
                    pointerType = ionic.Gestures.POINTER_MOUSE;
                }

                return {
                    center: ionic.Gestures.utils.getCenter(touches),
                    timeStamp: new Date().getTime(),
                    target: ev.target,
                    touches: touches,
                    eventType: eventType,
                    pointerType: pointerType,
                    srcEvent: ev,

                    /**
                     * prevent the browser default actions
                     * mostly used to disable scrolling of the browser
                     */
                    preventDefault: function () {
                        if (this.srcEvent.preventManipulation) {
                            this.srcEvent.preventManipulation();
                        }

                        if (this.srcEvent.preventDefault) {
                            // this.srcEvent.preventDefault();
                        }
                    },

                    /**
                     * stop bubbling the event up to its parents
                     */
                    stopPropagation: function () {
                        this.srcEvent.stopPropagation();
                    },

                    /**
                     * immediately stop gesture detection
                     * might be useful after a swipe was detected
                     * @return {*}
                     */
                    stopDetect: function () {
                        return ionic.Gestures.detection.stopDetect();
                    }
                };
            }
        };

        ionic.Gestures.PointerEvent = {
            /**
             * holds all pointers
             * type {Object}
             */
            pointers: {},

            /**
             * get a list of pointers
             * @returns {Array}     touchlist
             */
            getTouchList: function () {
                var self = this;
                var touchlist = [];

                // we can use forEach since pointerEvents only is in IE10
                Object.keys(self.pointers).sort().forEach(function (id) {
                    touchlist.push(self.pointers[id]);
                });
                return touchlist;
            },

            /**
             * update the position of a pointer
             * @param   {String}   type             ionic.Gestures.EVENT_END
             * @param   {Object}   pointerEvent
             */
            updatePointer: function (type, pointerEvent) {
                if (type == ionic.Gestures.EVENT_END) {
                    this.pointers = {};
                }
                else {
                    pointerEvent.identifier = pointerEvent.pointerId;
                    this.pointers[pointerEvent.pointerId] = pointerEvent;
                }

                return Object.keys(this.pointers).length;
            },

            /**
             * check if ev matches pointertype
             * @param   {String}        pointerType     ionic.Gestures.POINTER_MOUSE
             * @param   {PointerEvent}  ev
             */
            matchType: function (pointerType, ev) {
                if (!ev.pointerType) {
                    return false;
                }

                var types = {};
                types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE);
                types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH);
                types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN);
                return types[pointerType];
            },


            /**
             * get events
             */
            getEvents: function () {
                return [
                    'pointerdown MSPointerDown',
                    'pointermove MSPointerMove',
                    'pointerup pointercancel MSPointerUp MSPointerCancel'
                ];
            },

            /**
             * reset the list
             */
            reset: function () {
                this.pointers = {};
            }
        };


        ionic.Gestures.utils = {
            /**
             * extend method,
             * also used for cloning when dest is an empty object
             * @param   {Object}    dest
             * @param   {Object}    src
             * @param    {Boolean}    merge        do a merge
             * @returns {Object}    dest
             */
            extend: function extend(dest, src, merge) {
                for (var key in src) {
                    if (dest[key] !== undefined && merge) {
                        continue;
                    }
                    dest[key] = src[key];
                }
                return dest;
            },


            /**
             * find if a node is in the given parent
             * used for event delegation tricks
             * @param   {HTMLElement}   node
             * @param   {HTMLElement}   parent
             * @returns {boolean}       has_parent
             */
            hasParent: function (node, parent) {
                while (node) {
                    if (node == parent) {
                        return true;
                    }
                    node = node.parentNode;
                }
                return false;
            },


            /**
             * get the center of all the touches
             * @param   {Array}     touches
             * @returns {Object}    center
             */
            getCenter: function getCenter(touches) {
                var valuesX = [], valuesY = [];

                for (var t = 0, len = touches.length; t < len; t++) {
                    valuesX.push(touches[t].pageX);
                    valuesY.push(touches[t].pageY);
                }

                return {
                    pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
                    pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
                };
            },


            /**
             * calculate the velocity between two points
             * @param   {Number}    delta_time
             * @param   {Number}    delta_x
             * @param   {Number}    delta_y
             * @returns {Object}    velocity
             */
            getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
                return {
                    x: Math.abs(delta_x / delta_time) || 0,
                    y: Math.abs(delta_y / delta_time) || 0
                };
            },


            /**
             * calculate the angle between two coordinates
             * @param   {Touch}     touch1
             * @param   {Touch}     touch2
             * @returns {Number}    angle
             */
            getAngle: function getAngle(touch1, touch2) {
                var y = touch2.pageY - touch1.pageY,
                    x = touch2.pageX - touch1.pageX;
                return Math.atan2(y, x) * 180 / Math.PI;
            },


            /**
             * angle to direction define
             * @param   {Touch}     touch1
             * @param   {Touch}     touch2
             * @returns {String}    direction constant, like ionic.Gestures.DIRECTION_LEFT
             */
            getDirection: function getDirection(touch1, touch2) {
                var x = Math.abs(touch1.pageX - touch2.pageX),
                    y = Math.abs(touch1.pageY - touch2.pageY);

                if (x >= y) {
                    return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
                }
                else {
                    return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
                }
            },


            /**
             * calculate the distance between two touches
             * @param   {Touch}     touch1
             * @param   {Touch}     touch2
             * @returns {Number}    distance
             */
            getDistance: function getDistance(touch1, touch2) {
                var x = touch2.pageX - touch1.pageX,
                    y = touch2.pageY - touch1.pageY;
                return Math.sqrt((x * x) + (y * y));
            },


            /**
             * calculate the scale factor between two touchLists (fingers)
             * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
             * @param   {Array}     start
             * @param   {Array}     end
             * @returns {Number}    scale
             */
            getScale: function getScale(start, end) {
                // need two fingers...
                if (start.length >= 2 && end.length >= 2) {
                    return this.getDistance(end[0], end[1]) /
                        this.getDistance(start[0], start[1]);
                }
                return 1;
            },


            /**
             * calculate the rotation degrees between two touchLists (fingers)
             * @param   {Array}     start
             * @param   {Array}     end
             * @returns {Number}    rotation
             */
            getRotation: function getRotation(start, end) {
                // need two fingers
                if (start.length >= 2 && end.length >= 2) {
                    return this.getAngle(end[1], end[0]) -
                        this.getAngle(start[1], start[0]);
                }
                return 0;
            },


            /**
             * boolean if the direction is vertical
             * @param    {String}    direction
             * @returns  {Boolean}   is_vertical
             */
            isVertical: function isVertical(direction) {
                return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN);
            },


            /**
             * stop browser default behavior with css class
             * @param   {HtmlElement}   element
             * @param   {Object}        css_class
             */
            stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) {
                // changed from making many style changes to just adding a preset classname
                // less DOM manipulations, less code, and easier to control in the CSS side of things
                // hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method
                if (element && element.classList) {
                    element.classList.add(css_class);
                    element.onselectstart = function () {
                        return false;
                    };
                }
            }
        };


        ionic.Gestures.detection = {
            // contains all registred ionic.Gestures.gestures in the correct order
            gestures: [],

            // data of the current ionic.Gestures.gesture detection session
            current: null,

            // the previous ionic.Gestures.gesture session data
            // is a full clone of the previous gesture.current object
            previous: null,

            // when this becomes true, no gestures are fired
            stopped: false,


            /**
             * start ionic.Gestures.gesture detection
             * @param   {ionic.Gestures.Instance}   inst
             * @param   {Object}            eventData
             */
            startDetect: function startDetect(inst, eventData) {
                // already busy with a ionic.Gestures.gesture detection on an element
                if (this.current) {
                    return;
                }

                this.stopped = false;

                this.current = {
                    inst: inst, // reference to ionic.GesturesInstance we're working for
                    startEvent: ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc
                    lastEvent: false, // last eventData
                    name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc
                };

                this.detect(eventData);
            },


            /**
             * ionic.Gestures.gesture detection
             * @param   {Object}    eventData
             */
            detect: function detect(eventData) {
                if (!this.current || this.stopped) {
                    return null;
                }

                // extend event data with calculations about scale, distance etc
                eventData = this.extendEventData(eventData);

                // instance options
                var inst_options = this.current.inst.options;

                // call ionic.Gestures.gesture handlers
                for (var g = 0, len = this.gestures.length; g < len; g++) {
                    var gesture = this.gestures[g];

                    // only when the instance options have enabled this gesture
                    if (!this.stopped && inst_options[gesture.name] !== false) {
                        // if a handler returns false, we stop with the detection
                        if (gesture.handler.call(gesture, eventData, this.current.inst) === false) {
                            this.stopDetect();
                            break;
                        }
                    }
                }

                // store as previous event event
                if (this.current) {
                    this.current.lastEvent = eventData;
                }

                // endevent, but not the last touch, so dont stop
                if (eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length - 1) {
                    this.stopDetect();
                }

                return eventData;
            },


            /**
             * clear the ionic.Gestures.gesture vars
             * this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected
             * to stop other ionic.Gestures.gestures from being fired
             */
            stopDetect: function stopDetect() {
                // clone current data to the store as the previous gesture
                // used for the double tap gesture, since this is an other gesture detect session
                this.previous = ionic.Gestures.utils.extend({}, this.current);

                // reset the current
                this.current = null;

                // stopped!
                this.stopped = true;
            },


            /**
             * extend eventData for ionic.Gestures.gestures
             * @param   {Object}   ev
             * @returns {Object}   ev
             */
            extendEventData: function extendEventData(ev) {
                var startEv = this.current.startEvent;

                // if the touches change, set the new touches over the startEvent touches
                // this because touchevents don't have all the touches on touchstart, or the
                // user must place his fingers at the EXACT same time on the screen, which is not realistic
                // but, sometimes it happens that both fingers are touching at the EXACT same time
                if (startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
                    // extend 1 level deep to get the touchlist with the touch objects
                    startEv.touches = [];
                    for (var i = 0, len = ev.touches.length; i < len; i++) {
                        startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i]));
                    }
                }

                var delta_time = ev.timeStamp - startEv.timeStamp,
                    delta_x = ev.center.pageX - startEv.center.pageX,
                    delta_y = ev.center.pageY - startEv.center.pageY,
                    velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y);

                ionic.Gestures.utils.extend(ev, {
                    deltaTime: delta_time,
                    deltaX: delta_x,
                    deltaY: delta_y,

                    velocityX: velocity.x,
                    velocityY: velocity.y,

                    distance: ionic.Gestures.utils.getDistance(startEv.center, ev.center),
                    angle: ionic.Gestures.utils.getAngle(startEv.center, ev.center),
                    direction: ionic.Gestures.utils.getDirection(startEv.center, ev.center),

                    scale: ionic.Gestures.utils.getScale(startEv.touches, ev.touches),
                    rotation: ionic.Gestures.utils.getRotation(startEv.touches, ev.touches),

                    startEvent: startEv
                });

                return ev;
            },


            /**
             * register new gesture
             * @param   {Object}    gesture object, see gestures.js for documentation
             * @returns {Array}     gestures
             */
            register: function register(gesture) {
                // add an enable gesture options if there is no given
                var options = gesture.defaults || {};
                if (options[gesture.name] === undefined) {
                    options[gesture.name] = true;
                }

                // extend ionic.Gestures default options with the ionic.Gestures.gesture options
                ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true);

                // set its index
                gesture.index = gesture.index || 1000;

                // add ionic.Gestures.gesture to the list
                this.gestures.push(gesture);

                // sort the list by index
                this.gestures.sort(function (a, b) {
                    if (a.index < b.index) {
                        return -1;
                    }
                    if (a.index > b.index) {
                        return 1;
                    }
                    return 0;
                });

                return this.gestures;
            }
        };


        ionic.Gestures.gestures = ionic.Gestures.gestures || {};

        /**
         * Custom gestures
         * ==============================
         *
         * Gesture object
         * --------------------
         * The object structure of a gesture:
         *
         * { name: 'mygesture',
   *   index: 1337,
   *   defaults: {
   *     mygesture_option: true
   *   }
   *   handler: function(type, ev, inst) {
   *     // trigger gesture event
   *     inst.trigger(this.name, ev);
   *   }
   * }

         * @param   {String}    name
         * this should be the name of the gesture, lowercase
         * it is also being used to disable/enable the gesture per instance config.
         *
         * @param   {Number}    [index=1000]
         * the index of the gesture, where it is going to be in the stack of gestures detection
         * like when you build an gesture that depends on the drag gesture, it is a good
         * idea to place it after the index of the drag gesture.
         *
         * @param   {Object}    [defaults={}]
         * the default settings of the gesture. these are added to the instance settings,
         * and can be overruled per instance. you can also add the name of the gesture,
         * but this is also added by default (and set to true).
         *
         * @param   {Function}  handler
         * this handles the gesture detection of your custom gesture and receives the
         * following arguments:
         *
         *      @param  {Object}    eventData
         *      event data containing the following properties:
         *          timeStamp   {Number}        time the event occurred
         *          target      {HTMLElement}   target element
         *          touches     {Array}         touches (fingers, pointers, mouse) on the screen
         *          pointerType {String}        kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH
         *          center      {Object}        center position of the touches. contains pageX and pageY
         *          deltaTime   {Number}        the total time of the touches in the screen
         *          deltaX      {Number}        the delta on x axis we haved moved
         *          deltaY      {Number}        the delta on y axis we haved moved
         *          velocityX   {Number}        the velocity on the x
         *          velocityY   {Number}        the velocity on y
         *          angle       {Number}        the angle we are moving
         *          direction   {String}        the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT
         *          distance    {Number}        the distance we haved moved
         *          scale       {Number}        scaling of the touches, needs 2 touches
         *          rotation    {Number}        rotation of the touches, needs 2 touches *
         *          eventType   {String}        matches ionic.Gestures.EVENT_START|MOVE|END
         *          srcEvent    {Object}        the source event, like TouchStart or MouseDown *
         *          startEvent  {Object}        contains the same properties as above,
         *                                      but from the first touch. this is used to calculate
         *                                      distances, deltaTime, scaling etc
         *
         *      @param  {ionic.Gestures.Instance}    inst
         *      the instance we are doing the detection for. you can get the options from
         *      the inst.options object and trigger the gesture event by calling inst.trigger
         *
         *
         * Handle gestures
         * --------------------
         * inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current
         * detection sessionic. It has the following properties
         *      @param  {String}    name
         *      contains the name of the gesture we have detected. it has not a real function,
         *      only to check in other gestures if something is detected.
         *      like in the drag gesture we set it to 'drag' and in the swipe gesture we can
         *      check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name
         *
         *      readonly
         *      @param  {ionic.Gestures.Instance}    inst
         *      the instance we do the detection for
         *
         *      readonly
         *      @param  {Object}    startEvent
         *      contains the properties of the first gesture detection in this sessionic.
         *      Used for calculations about timing, distance, etc.
         *
         *      readonly
         *      @param  {Object}    lastEvent
         *      contains all the properties of the last gesture detect in this sessionic.
         *
         * after the gesture detection session has been completed (user has released the screen)
         * the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous,
         * this is usefull for gestures like doubletap, where you need to know if the
         * previous gesture was a tap
         *
         * options that have been set by the instance can be received by calling inst.options
         *
         * You can trigger a gesture event by calling inst.trigger("mygesture", event).
         * The first param is the name of your gesture, the second the event argument
         *
         *
         * Register gestures
         * --------------------
         * When an gesture is added to the ionic.Gestures.gestures object, it is auto registered
         * at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register
         * manually and pass your gesture object as a param
         *
         */

        /**
         * Hold
         * Touch stays at the same place for x time
         * events  hold
         */
        ionic.Gestures.gestures.Hold = {
            name: 'hold',
            index: 10,
            defaults: {
                hold_timeout: 500,
                hold_threshold: 9
            },
            timer: null,
            handler: function holdGesture(ev, inst) {
                switch (ev.eventType) {
                    case ionic.Gestures.EVENT_START:
                        // clear any running timers
                        clearTimeout(this.timer);

                        // set the gesture so we can check in the timeout if it still is
                        ionic.Gestures.detection.current.name = this.name;

                        // set timer and if after the timeout it still is hold,
                        // we trigger the hold event
                        this.timer = setTimeout(function () {
                            if (ionic.Gestures.detection.current.name == 'hold') {
                                ionic.tap.cancelClick();
                                inst.trigger('hold', ev);
                            }
                        }, inst.options.hold_timeout);
                        break;

                    // when you move or end we clear the timer
                    case ionic.Gestures.EVENT_MOVE:
                        if (ev.distance > inst.options.hold_threshold) {
                            clearTimeout(this.timer);
                        }
                        break;

                    case ionic.Gestures.EVENT_END:
                        clearTimeout(this.timer);
                        break;
                }
            }
        };


        /**
         * Tap/DoubleTap
         * Quick touch at a place or double at the same place
         * events  tap, doubletap
         */
        ionic.Gestures.gestures.Tap = {
            name: 'tap',
            index: 100,
            defaults: {
                tap_max_touchtime: 250,
                tap_max_distance: 10,
                tap_always: true,
                doubletap_distance: 20,
                doubletap_interval: 300
            },
            handler: function tapGesture(ev, inst) {
                if (ev.eventType == ionic.Gestures.EVENT_END && ev.srcEvent.type != 'touchcancel') {
                    // previous gesture, for the double tap since these are two different gesture detections
                    var prev = ionic.Gestures.detection.previous,
                        did_doubletap = false;

                    // when the touchtime is higher then the max touch time
                    // or when the moving distance is too much
                    if (ev.deltaTime > inst.options.tap_max_touchtime ||
                        ev.distance > inst.options.tap_max_distance) {
                        return;
                    }

                    // check if double tap
                    if (prev && prev.name == 'tap' &&
                        (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
                        ev.distance < inst.options.doubletap_distance) {
                        inst.trigger('doubletap', ev);
                        did_doubletap = true;
                    }

                    // do a single tap
                    if (!did_doubletap || inst.options.tap_always) {
                        ionic.Gestures.detection.current.name = 'tap';
                        inst.trigger('tap', ev);
                    }
                }
            }
        };


        /**
         * Swipe
         * triggers swipe events when the end velocity is above the threshold
         * events  swipe, swipeleft, swiperight, swipeup, swipedown
         */
        ionic.Gestures.gestures.Swipe = {
            name: 'swipe',
            index: 40,
            defaults: {
                // set 0 for unlimited, but this can conflict with transform
                swipe_max_touches: 1,
                swipe_velocity: 0.4
            },
            handler: function swipeGesture(ev, inst) {
                if (ev.eventType == ionic.Gestures.EVENT_END) {
                    // max touches
                    if (inst.options.swipe_max_touches > 0 &&
                        ev.touches.length > inst.options.swipe_max_touches) {
                        return;
                    }

                    // when the distance we moved is too small we skip this gesture
                    // or we can be already in dragging
                    if (ev.velocityX > inst.options.swipe_velocity ||
                        ev.velocityY > inst.options.swipe_velocity) {
                        // trigger swipe events
                        inst.trigger(this.name, ev);
                        inst.trigger(this.name + ev.direction, ev);
                    }
                }
            }
        };


        /**
         * Drag
         * Move with x fingers (default 1) around on the page. Blocking the scrolling when
         * moving left and right is a good practice. When all the drag events are blocking
         * you disable scrolling on that area.
         * events  drag, drapleft, dragright, dragup, dragdown
         */
        ionic.Gestures.gestures.Drag = {
            name: 'drag',
            index: 50,
            defaults: {
                drag_min_distance: 10,
                // Set correct_for_drag_min_distance to true to make the starting point of the drag
                // be calculated from where the drag was triggered, not from where the touch started.
                // Useful to avoid a jerk-starting drag, which can make fine-adjustments
                // through dragging difficult, and be visually unappealing.
                correct_for_drag_min_distance: true,
                // set 0 for unlimited, but this can conflict with transform
                drag_max_touches: 1,
                // prevent default browser behavior when dragging occurs
                // be careful with it, it makes the element a blocking element
                // when you are using the drag gesture, it is a good practice to set this true
                drag_block_horizontal: true,
                drag_block_vertical: true,
                // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
                // It disallows vertical directions if the initial direction was horizontal, and vice versa.
                drag_lock_to_axis: false,
                // drag lock only kicks in when distance > drag_lock_min_distance
                // This way, locking occurs only when the distance has become large enough to reliably determine the direction
                drag_lock_min_distance: 25,
                // prevent default if the gesture is going the given direction
                prevent_default_directions: []
            },
            triggered: false,
            handler: function dragGesture(ev, inst) {
                if (ev.srcEvent.type == 'touchstart' || ev.srcEvent.type == 'touchend') {
                    this.preventedFirstMove = false;

                } else if (!this.preventedFirstMove && ev.srcEvent.type == 'touchmove') {
                    // Prevent gestures that are not intended for this event handler from firing subsequent times
                    if (inst.options.prevent_default_directions.length > 0
                        && inst.options.prevent_default_directions.indexOf(ev.direction) != -1) {
                        ev.srcEvent.preventDefault();
                    }
                    this.preventedFirstMove = true;
                }

                // current gesture isnt drag, but dragged is true
                // this means an other gesture is busy. now call dragend
                if (ionic.Gestures.detection.current.name != this.name && this.triggered) {
                    inst.trigger(this.name + 'end', ev);
                    this.triggered = false;
                    return;
                }

                // max touches
                if (inst.options.drag_max_touches > 0 &&
                    ev.touches.length > inst.options.drag_max_touches) {
                    return;
                }

                switch (ev.eventType) {
                    case ionic.Gestures.EVENT_START:
                        this.triggered = false;
                        break;

                    case ionic.Gestures.EVENT_MOVE:
                        // when the distance we moved is too small we skip this gesture
                        // or we can be already in dragging
                        if (ev.distance < inst.options.drag_min_distance &&
                            ionic.Gestures.detection.current.name != this.name) {
                            return;
                        }

                        // we are dragging!
                        if (ionic.Gestures.detection.current.name != this.name) {
                            ionic.Gestures.detection.current.name = this.name;
                            if (inst.options.correct_for_drag_min_distance) {
                                // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
                                // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
                                // It might be useful to save the original start point somewhere
                                var factor = Math.abs(inst.options.drag_min_distance / ev.distance);
                                ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor;
                                ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor;

                                // recalculate event data using new start point
                                ev = ionic.Gestures.detection.extendEventData(ev);
                            }
                        }

                        // lock drag to axis?
                        if (ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance <= ev.distance)) {
                            ev.drag_locked_to_axis = true;
                        }
                        var last_direction = ionic.Gestures.detection.current.lastEvent.direction;
                        if (ev.drag_locked_to_axis && last_direction !== ev.direction) {
                            // keep direction on the axis that the drag gesture started on
                            if (ionic.Gestures.utils.isVertical(last_direction)) {
                                ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
                            }
                            else {
                                ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
                            }
                        }

                        // first time, trigger dragstart event
                        if (!this.triggered) {
                            inst.trigger(this.name + 'start', ev);
                            this.triggered = true;
                        }

                        // trigger normal event
                        inst.trigger(this.name, ev);

                        // direction event, like dragdown
                        inst.trigger(this.name + ev.direction, ev);

                        // block the browser events
                        if ((inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) ||
                            (inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) {
                            ev.preventDefault();
                        }
                        break;

                    case ionic.Gestures.EVENT_END:
                        // trigger dragend
                        if (this.triggered) {
                            inst.trigger(this.name + 'end', ev);
                        }

                        this.triggered = false;
                        break;
                }
            }
        };


        /**
         * Transform
         * User want to scale or rotate with 2 fingers
         * events  transform, pinch, pinchin, pinchout, rotate
         */
        ionic.Gestures.gestures.Transform = {
            name: 'transform',
            index: 45,
            defaults: {
                // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
                transform_min_scale: 0.01,
                // rotation in degrees
                transform_min_rotation: 1,
                // prevent default browser behavior when two touches are on the screen
                // but it makes the element a blocking element
                // when you are using the transform gesture, it is a good practice to set this true
                transform_always_block: false
            },
            triggered: false,
            handler: function transformGesture(ev, inst) {
                // current gesture isnt drag, but dragged is true
                // this means an other gesture is busy. now call dragend
                if (ionic.Gestures.detection.current.name != this.name && this.triggered) {
                    inst.trigger(this.name + 'end', ev);
                    this.triggered = false;
                    return;
                }

                // atleast multitouch
                if (ev.touches.length < 2) {
                    return;
                }

                // prevent default when two fingers are on the screen
                if (inst.options.transform_always_block) {
                    ev.preventDefault();
                }

                switch (ev.eventType) {
                    case ionic.Gestures.EVENT_START:
                        this.triggered = false;
                        break;

                    case ionic.Gestures.EVENT_MOVE:
                        var scale_threshold = Math.abs(1 - ev.scale);
                        var rotation_threshold = Math.abs(ev.rotation);

                        // when the distance we moved is too small we skip this gesture
                        // or we can be already in dragging
                        if (scale_threshold < inst.options.transform_min_scale &&
                            rotation_threshold < inst.options.transform_min_rotation) {
                            return;
                        }

                        // we are transforming!
                        ionic.Gestures.detection.current.name = this.name;

                        // first time, trigger dragstart event
                        if (!this.triggered) {
                            inst.trigger(this.name + 'start', ev);
                            this.triggered = true;
                        }

                        inst.trigger(this.name, ev); // basic transform event

                        // trigger rotate event
                        if (rotation_threshold > inst.options.transform_min_rotation) {
                            inst.trigger('rotate', ev);
                        }

                        // trigger pinch event
                        if (scale_threshold > inst.options.transform_min_scale) {
                            inst.trigger('pinch', ev);
                            inst.trigger('pinch' + ((ev.scale < 1) ? 'in' : 'out'), ev);
                        }
                        break;

                    case ionic.Gestures.EVENT_END:
                        // trigger dragend
                        if (this.triggered) {
                            inst.trigger(this.name + 'end', ev);
                        }

                        this.triggered = false;
                        break;
                }
            }
        };


        /**
         * Touch
         * Called as first, tells the user has touched the screen
         * events  touch
         */
        ionic.Gestures.gestures.Touch = {
            name: 'touch',
            index: -Infinity,
            defaults: {
                // call preventDefault at touchstart, and makes the element blocking by
                // disabling the scrolling of the page, but it improves gestures like
                // transforming and dragging.
                // be careful with using this, it can be very annoying for users to be stuck
                // on the page
                prevent_default: false,

                // disable mouse events, so only touch (or pen!) input triggers events
                prevent_mouseevents: false
            },
            handler: function touchGesture(ev, inst) {
                if (inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) {
                    ev.stopDetect();
                    return;
                }

                if (inst.options.prevent_default) {
                    ev.preventDefault();
                }

                if (ev.eventType == ionic.Gestures.EVENT_START) {
                    inst.trigger(this.name, ev);
                }
            }
        };


        /**
         * Release
         * Called as last, tells the user has released the screen
         * events  release
         */
        ionic.Gestures.gestures.Release = {
            name: 'release',
            index: Infinity,
            handler: function releaseGesture(ev, inst) {
                if (ev.eventType == ionic.Gestures.EVENT_END) {
                    inst.trigger(this.name, ev);
                }
            }
        };
    })(window.ionic);

    (function (window, document, ionic) {

        function getParameterByName(name) {
            name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
            var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
                results = regex.exec(location.search);
            return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
        }

        var IOS = 'ios';
        var ANDROID = 'android';
        var WINDOWS_PHONE = 'windowsphone';
        var EDGE = 'edge';
        var CROSSWALK = 'crosswalk';
        var requestAnimationFrame = ionic.requestAnimationFrame;

        /**
         * @ngdoc utility
         * @name ionic.Platform
         * @module ionic
         * @description
         * A set of utility methods that can be used to retrieve the device ready state and
         * various other information such as what kind of platform the app is currently installed on.
         *
         * @usage
         * ```js
         * angular.module('PlatformApp', ['ionic'])
         * .controller('PlatformCtrl', function($scope) {
   *
   *   ionic.Platform.ready(function(){
   *     // will execute when device is ready, or immediately if the device is already ready.
   *   });
   *
   *   var deviceInformation = ionic.Platform.device();
   *
   *   var isWebView = ionic.Platform.isWebView();
   *   var isIPad = ionic.Platform.isIPad();
   *   var isIOS = ionic.Platform.isIOS();
   *   var isAndroid = ionic.Platform.isAndroid();
   *   var isWindowsPhone = ionic.Platform.isWindowsPhone();
   *
   *   var currentPlatform = ionic.Platform.platform();
   *   var currentPlatformVersion = ionic.Platform.version();
   *
   *   ionic.Platform.exitApp(); // stops the app
   * });
         * ```
         */
        var self = ionic.Platform = {

            // Put navigator on platform so it can be mocked and set
            // the browser does not allow window.navigator to be set
            navigator: window.navigator,

            /**
             * @ngdoc property
             * @name ionic.Platform#isReady
             * @returns {boolean} Whether the device is ready.
             */
            isReady: false,
            /**
             * @ngdoc property
             * @name ionic.Platform#isFullScreen
             * @returns {boolean} Whether the device is fullscreen.
             */
            isFullScreen: false,
            /**
             * @ngdoc property
             * @name ionic.Platform#platforms
             * @returns {Array(string)} An array of all platforms found.
             */
            platforms: null,
            /**
             * @ngdoc property
             * @name ionic.Platform#grade
             * @returns {string} What grade the current platform is.
             */
            grade: null,
            /**
             * @ngdoc property
             * @name ionic.Platform#ua
             * @returns {string} What User Agent is.
             */
            ua: navigator.userAgent,

            /**
             * @ngdoc method
             * @name ionic.Platform#ready
             * @description
             * Trigger a callback once the device is ready, or immediately
             * if the device is already ready. This method can be run from
             * anywhere and does not need to be wrapped by any additonal methods.
             * When the app is within a WebView (Cordova), it'll fire
             * the callback once the device is ready. If the app is within
             * a web browser, it'll fire the callback after `window.load`.
             * Please remember that Cordova features (Camera, FileSystem, etc) still
             * will not work in a web browser.
             * @param {function} callback The function to call.
             */
            ready: function (cb) {
                // run through tasks to complete now that the device is ready
                if (self.isReady) {
                    cb();
                } else {
                    // the platform isn't ready yet, add it to this array
                    // which will be called once the platform is ready
                    readyCallbacks.push(cb);
                }
            },

            /**
             * @private
             */
            detect: function () {
                self._checkPlatforms();

                requestAnimationFrame(function () {
                    // only add to the body class if we got platform info
                    for (var i = 0; i < self.platforms.length; i++) {
                        document.body.classList.add('platform-' + self.platforms[i]);
                    }
                });
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#setGrade
             * @description Set the grade of the device: 'a', 'b', or 'c'. 'a' is the best
             * (most css features enabled), 'c' is the worst.  By default, sets the grade
             * depending on the current device.
             * @param {string} grade The new grade to set.
             */
            setGrade: function (grade) {
                var oldGrade = self.grade;
                self.grade = grade;
                requestAnimationFrame(function () {
                    if (oldGrade) {
                        document.body.classList.remove('grade-' + oldGrade);
                    }
                    document.body.classList.add('grade-' + grade);
                });
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#device
             * @description Return the current device (given by cordova).
             * @returns {object} The device object.
             */
            device: function () {
                return window.device || {};
            },

            _checkPlatforms: function () {
                self.platforms = [];
                var grade = 'a';

                if (self.isWebView()) {
                    self.platforms.push('webview');
                    if (!(!window.cordova && !window.PhoneGap && !window.phonegap)) {
                        self.platforms.push('cordova');
                    } else if (typeof window.forge === 'object') {
                        self.platforms.push('trigger');
                    }
                } else {
                    self.platforms.push('browser');
                }
                if (self.isIPad()) self.platforms.push('ipad');

                var platform = self.platform();
                if (platform) {
                    self.platforms.push(platform);

                    var version = self.version();
                    if (version) {
                        var v = version.toString();
                        if (v.indexOf('.') > 0) {
                            v = v.replace('.', '_');
                        } else {
                            v += '_0';
                        }
                        self.platforms.push(platform + v.split('_')[0]);
                        self.platforms.push(platform + v);

                        if (self.isAndroid() && version < 4.4) {
                            grade = (version < 4 ? 'c' : 'b');
                        } else if (self.isWindowsPhone()) {
                            grade = 'b';
                        }
                    }
                }

                self.setGrade(grade);
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#isWebView
             * @returns {boolean} Check if we are running within a WebView (such as Cordova).
             */
            isWebView: function () {
                return !(!window.cordova && !window.PhoneGap && !window.phonegap && window.forge !== 'object');
            },
            /**
             * @ngdoc method
             * @name ionic.Platform#isIPad
             * @returns {boolean} Whether we are running on iPad.
             */
            isIPad: function () {
                if (/iPad/i.test(self.navigator.platform)) {
                    return true;
                }
                return /iPad/i.test(self.ua);
            },
            /**
             * @ngdoc method
             * @name ionic.Platform#isIOS
             * @returns {boolean} Whether we are running on iOS.
             */
            isIOS: function () {
                return self.is(IOS);
            },
            /**
             * @ngdoc method
             * @name ionic.Platform#isAndroid
             * @returns {boolean} Whether we are running on Android.
             */
            isAndroid: function () {
                return self.is(ANDROID);
            },
            /**
             * @ngdoc method
             * @name ionic.Platform#isWindowsPhone
             * @returns {boolean} Whether we are running on Windows Phone.
             */
            isWindowsPhone: function () {
                return self.is(WINDOWS_PHONE);
            },
            /**
             * @ngdoc method
             * @name ionic.Platform#isEdge
             * @returns {boolean} Whether we are running on MS Edge/Windows 10 (inc. Phone)
             */
            isEdge: function () {
                return self.is(EDGE);
            },

            isCrosswalk: function () {
                return self.is(CROSSWALK);
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#platform
             * @returns {string} The name of the current platform.
             */
            platform: function () {
                // singleton to get the platform name
                if (platformName === null) self.setPlatform(self.device().platform);
                return platformName;
            },

            /**
             * @private
             */
            setPlatform: function (n) {
                if (typeof n != 'undefined' && n !== null && n.length) {
                    platformName = n.toLowerCase();
                } else if (getParameterByName('ionicplatform')) {
                    platformName = getParameterByName('ionicplatform');
                } else if (self.ua.indexOf('Edge') > -1) {
                    platformName = EDGE;
                } else if (self.ua.indexOf('Windows Phone') > -1) {
                    platformName = WINDOWS_PHONE;
                } else if (self.ua.indexOf('Android') > 0) {
                    platformName = ANDROID;
                } else if (/iPhone|iPad|iPod/.test(self.ua)) {
                    platformName = IOS;
                } else {
                    platformName = self.navigator.platform && navigator.platform.toLowerCase().split(' ')[0] || '';
                }
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#version
             * @returns {number} The version of the current device platform.
             */
            version: function () {
                // singleton to get the platform version
                if (platformVersion === null) self.setVersion(self.device().version);
                return platformVersion;
            },

            /**
             * @private
             */
            setVersion: function (v) {
                if (typeof v != 'undefined' && v !== null) {
                    v = v.split('.');
                    v = parseFloat(v[0] + '.' + (v.length > 1 ? v[1] : 0));
                    if (!isNaN(v)) {
                        platformVersion = v;
                        return;
                    }
                }

                platformVersion = 0;

                // fallback to user-agent checking
                var pName = self.platform();
                var versionMatch = {
                    'android': /Android (\d+).(\d+)?/,
                    'ios': /OS (\d+)_(\d+)?/,
                    'windowsphone': /Windows Phone (\d+).(\d+)?/
                };
                if (versionMatch[pName]) {
                    v = self.ua.match(versionMatch[pName]);
                    if (v && v.length > 2) {
                        platformVersion = parseFloat(v[1] + '.' + v[2]);
                    }
                }
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#is
             * @param {string} Platform name.
             * @returns {boolean} Whether the platform name provided is detected.
             */
            is: function (type) {
                type = type.toLowerCase();
                // check if it has an array of platforms
                if (self.platforms) {
                    for (var x = 0; x < self.platforms.length; x++) {
                        if (self.platforms[x] === type) return true;
                    }
                }
                // exact match
                var pName = self.platform();
                if (pName) {
                    return pName === type.toLowerCase();
                }

                // A quick hack for to check userAgent
                return self.ua.toLowerCase().indexOf(type) >= 0;
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#exitApp
             * @description Exit the app.
             */
            exitApp: function () {
                self.ready(function () {
                    navigator.app && navigator.app.exitApp && navigator.app.exitApp();
                });
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#showStatusBar
             * @description Shows or hides the device status bar (in Cordova). Requires `ionic plugin add cordova-plugin-statusbar`
             * @param {boolean} shouldShow Whether or not to show the status bar.
             */
            showStatusBar: function (val) {
                // Only useful when run within cordova
                self._showStatusBar = val;
                self.ready(function () {
                    // run this only when or if the platform (cordova) is ready
                    requestAnimationFrame(function () {
                        if (self._showStatusBar) {
                            // they do not want it to be full screen
                            window.StatusBar && window.StatusBar.show();
                            document.body.classList.remove('status-bar-hide');
                        } else {
                            // it should be full screen
                            window.StatusBar && window.StatusBar.hide();
                            document.body.classList.add('status-bar-hide');
                        }
                    });
                });
            },

            /**
             * @ngdoc method
             * @name ionic.Platform#fullScreen
             * @description
             * Sets whether the app is fullscreen or not (in Cordova).
             * @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `ionic plugin add cordova-plugin-statusbar`
             * @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false.
             */
            fullScreen: function (showFullScreen, showStatusBar) {
                // showFullScreen: default is true if no param provided
                self.isFullScreen = (showFullScreen !== false);

                // add/remove the fullscreen classname to the body
                ionic.DomUtil.ready(function () {
                    // run this only when or if the DOM is ready
                    requestAnimationFrame(function () {
                        if (self.isFullScreen) {
                            document.body.classList.add('fullscreen');
                        } else {
                            document.body.classList.remove('fullscreen');
                        }
                    });
                    // showStatusBar: default is false if no param provided
                    self.showStatusBar((showStatusBar === true));
                });
            }

        };

        var platformName = null, // just the name, like iOS or Android
            platformVersion = null, // a float of the major and minor, like 7.1
            readyCallbacks = [],
            windowLoadListenderAttached,
            platformReadyTimer = 2000; // How long to wait for platform ready before emitting a warning

        verifyPlatformReady();

        // Warn the user if deviceready did not fire in a reasonable amount of time, and how to fix it.
        function verifyPlatformReady() {
            setTimeout(function () {
                if (!self.isReady && self.isWebView()) {
                    void 0;
                }
            }, platformReadyTimer);
        }

        // setup listeners to know when the device is ready to go
        function onWindowLoad() {
            if (self.isWebView()) {
                // the window and scripts are fully loaded, and a cordova/phonegap
                // object exists then let's listen for the deviceready
                document.addEventListener("deviceready", onPlatformReady, false);
            } else {
                // the window and scripts are fully loaded, but the window object doesn't have the
                // cordova/phonegap object, so its just a browser, not a webview wrapped w/ cordova
                onPlatformReady();
            }
            if (windowLoadListenderAttached) {
                window.removeEventListener("load", onWindowLoad, false);
            }
        }

        if (document.readyState === 'complete') {
            onWindowLoad();
        } else {
            windowLoadListenderAttached = true;
            window.addEventListener("load", onWindowLoad, false);
        }

        function onPlatformReady() {
            // the device is all set to go, init our own stuff then fire off our event
            self.isReady = true;
            self.detect();
            for (var x = 0; x < readyCallbacks.length; x++) {
                // fire off all the callbacks that were added before the platform was ready
                readyCallbacks[x]();
            }
            readyCallbacks = [];
            ionic.trigger('platformready', {target: document});

            requestAnimationFrame(function () {
                document.body.classList.add('platform-ready');
            });
        }

    })(window, document, ionic);

    (function (document, ionic) {
        'use strict';

        // Ionic CSS polyfills
        ionic.CSS = {};
        ionic.CSS.TRANSITION = [];
        ionic.CSS.TRANSFORM = [];

        ionic.EVENTS = {};

        (function () {

            // transform
            var i, keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform',
                '-moz-transform', 'moz-transform', 'MozTransform', 'mozTransform', 'msTransform'];

            for (i = 0; i < keys.length; i++) {
                if (document.documentElement.style[keys[i]] !== undefined) {
                    ionic.CSS.TRANSFORM = keys[i];
                    break;
                }
            }

            // transition
            keys = ['webkitTransition', 'mozTransition', 'msTransition', 'transition'];
            for (i = 0; i < keys.length; i++) {
                if (document.documentElement.style[keys[i]] !== undefined) {
                    ionic.CSS.TRANSITION = keys[i];
                    break;
                }
            }

            // Fallback in case the keys don't exist at all
            ionic.CSS.TRANSITION = ionic.CSS.TRANSITION || 'transition';

            // The only prefix we care about is webkit for transitions.
            var isWebkit = ionic.CSS.TRANSITION.indexOf('webkit') > -1;

            // transition duration
            ionic.CSS.TRANSITION_DURATION = (isWebkit ? '-webkit-' : '') + 'transition-duration';

            // To be sure transitionend works everywhere, include *both* the webkit and non-webkit events
            ionic.CSS.TRANSITIONEND = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend';
        })();

        (function () {
            var touchStartEvent = 'touchstart';
            var touchMoveEvent = 'touchmove';
            var touchEndEvent = 'touchend';
            var touchCancelEvent = 'touchcancel';

            if (window.navigator.pointerEnabled) {
                touchStartEvent = 'pointerdown';
                touchMoveEvent = 'pointermove';
                touchEndEvent = 'pointerup';
                touchCancelEvent = 'pointercancel';
            } else if (window.navigator.msPointerEnabled) {
                touchStartEvent = 'MSPointerDown';
                touchMoveEvent = 'MSPointerMove';
                touchEndEvent = 'MSPointerUp';
                touchCancelEvent = 'MSPointerCancel';
            }

            ionic.EVENTS.touchstart = touchStartEvent;
            ionic.EVENTS.touchmove = touchMoveEvent;
            ionic.EVENTS.touchend = touchEndEvent;
            ionic.EVENTS.touchcancel = touchCancelEvent;
        })();

        // classList polyfill for them older Androids
        // https://gist.github.com/devongovett/1381839
        if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') {
            Object.defineProperty(HTMLElement.prototype, 'classList', {
                get: function () {
                    var self = this;

                    function update(fn) {
                        return function () {
                            var x, classes = self.className.split(/\s+/);

                            for (x = 0; x < arguments.length; x++) {
                                fn(classes, classes.indexOf(arguments[x]), arguments[x]);
                            }

                            self.className = classes.join(" ");
                        };
                    }

                    return {
                        add: update(function (classes, index, value) {
                            ~index || classes.push(value);
                        }),

                        remove: update(function (classes, index) {
                            ~index && classes.splice(index, 1);
                        }),

                        toggle: update(function (classes, index, value) {
                            ~index ? classes.splice(index, 1) : classes.push(value);
                        }),

                        contains: function (value) {
                            return !!~self.className.split(/\s+/).indexOf(value);
                        },

                        item: function (i) {
                            return self.className.split(/\s+/)[i] || null;
                        }
                    };

                }
            });
        }

    })(document, ionic);


    /**
     * @ngdoc page
     * @name tap
     * @module ionic
     * @description
     * On touch devices such as a phone or tablet, some browsers implement a 300ms delay between
     * the time the user stops touching the display and the moment the browser executes the
     * click. This delay was initially introduced so the browser can know whether the user wants to
     * double-tap to zoom in on the webpage.  Basically, the browser waits roughly 300ms to see if
     * the user is double-tapping, or just tapping on the display once.
     *
     * Out of the box, Ionic automatically removes the 300ms delay in order to make Ionic apps
     * feel more "native" like. Resultingly, other solutions such as
     * [fastclick](https://github.com/ftlabs/fastclick) and Angular's
     * [ngTouch](https://docs.angularjs.org/api/ngTouch) should not be included, to avoid conflicts.
     *
     * Some browsers already remove the delay with certain settings, such as the CSS property
     * `touch-events: none` or with specific meta tag viewport values. However, each of these
     * browsers still handle clicks differently, such as when to fire off or cancel the event
     * (like scrolling when the target is a button, or holding a button down).
     * For browsers that already remove the 300ms delay, consider Ionic's tap system as a way to
     * normalize how clicks are handled across the various devices so there's an expected response
     * no matter what the device, platform or version. Additionally, Ionic will prevent
     * ghostclicks which even browsers that remove the delay still experience.
     *
     * In some cases, third-party libraries may also be working with touch events which can interfere
     * with the tap system. For example, mapping libraries like Google or Leaflet Maps often implement
     * a touch detection system which conflicts with Ionic's tap system.
     *
     * ### Disabling the tap system
     *
     * To disable the tap for an element and all of its children elements,
     * add the attribute `data-tap-disabled="true"`.
     *
     * ```html
     * <div data-tap-disabled="true">
     *     <div id="google-map"></div>
     * </div>
     * ```
     *
     * ### Additional Notes:
     *
     * - Ionic tap  works with Ionic's JavaScript scrolling
     * - Elements can come and go from the DOM and Ionic tap doesn't keep adding and removing
     *   listeners
     * - No "tap delay" after the first "tap" (you can tap as fast as you want, they all click)
     * - Minimal events listeners, only being added to document
     * - Correct focus in/out on each input type (select, textearea, range) on each platform/device
     * - Shows and hides virtual keyboard correctly for each platform/device
     * - Works with labels surrounding inputs
     * - Does not fire off a click if the user moves the pointer too far
     * - Adds and removes an 'activated' css class
     * - Multiple [unit tests](https://github.com/driftyco/ionic/blob/1.x/test/unit/utils/tap.unit.js) for each scenario
     *
     */
    /*

     IONIC TAP
     ---------------
     - Both touch and mouse events are added to the document.body on DOM ready
     - If a touch event happens, it does not use mouse event listeners
     - On touchend, if the distance between start and end was small, trigger a click
     - In the triggered click event, add a 'isIonicTap' property
     - The triggered click receives the same x,y coordinates as as the end event
     - On document.body click listener (with useCapture=true), only allow clicks with 'isIonicTap'
     - Triggering clicks with mouse events work the same as touch, except with mousedown/mouseup
     - Tapping inputs is disabled during scrolling
     */

    var tapDoc; // the element which the listeners are on (document.body)
    var tapActiveEle; // the element which is active (probably has focus)
    var tapEnabledTouchEvents;
    var tapMouseResetTimer;
    var tapPointerMoved;
    var tapPointerStart;
    var tapTouchFocusedInput;
    var tapLastTouchTarget;
    var tapTouchMoveListener = 'touchmove';

// how much the coordinates can be off between start/end, but still a click
    var TAP_RELEASE_TOLERANCE = 12; // default tolerance
    var TAP_RELEASE_BUTTON_TOLERANCE = 50; // button elements should have a larger tolerance

    var tapEventListeners = {
        'click': tapClickGateKeeper,

        'mousedown': tapMouseDown,
        'mouseup': tapMouseUp,
        'mousemove': tapMouseMove,

        'touchstart': tapTouchStart,
        'touchend': tapTouchEnd,
        'touchcancel': tapTouchCancel,
        'touchmove': tapTouchMove,

        'pointerdown': tapTouchStart,
        'pointerup': tapTouchEnd,
        'pointercancel': tapTouchCancel,
        'pointermove': tapTouchMove,

        'MSPointerDown': tapTouchStart,
        'MSPointerUp': tapTouchEnd,
        'MSPointerCancel': tapTouchCancel,
        'MSPointerMove': tapTouchMove,

        'focusin': tapFocusIn,
        'focusout': tapFocusOut
    };

    ionic.tap = {

        register: function (ele) {
            tapDoc = ele;

            tapEventListener('click', true, true);
            tapEventListener('mouseup');
            tapEventListener('mousedown');

            if (window.navigator.pointerEnabled) {
                tapEventListener('pointerdown');
                tapEventListener('pointerup');
                tapEventListener('pointercancel');
                tapTouchMoveListener = 'pointermove';

            } else if (window.navigator.msPointerEnabled) {
                tapEventListener('MSPointerDown');
                tapEventListener('MSPointerUp');
                tapEventListener('MSPointerCancel');
                tapTouchMoveListener = 'MSPointerMove';

            } else {
                tapEventListener('touchstart');
                tapEventListener('touchend');
                tapEventListener('touchcancel');
            }

            tapEventListener('focusin');
            tapEventListener('focusout');

            return function () {
                for (var type in tapEventListeners) {
                    tapEventListener(type, false);
                }
                tapDoc = null;
                tapActiveEle = null;
                tapEnabledTouchEvents = false;
                tapPointerMoved = false;
                tapPointerStart = null;
            };
        },

        ignoreScrollStart: function (e) {
            return (e.defaultPrevented) ||  // defaultPrevented has been assigned by another component handling the event
                (/^(file|range)$/i).test(e.target.type) ||
                (e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll')) == 'true' || // manually set within an elements attributes
                (!!(/^(object|embed)$/i).test(e.target.tagName)) ||  // flash/movie/object touches should not try to scroll
                ionic.tap.isElementTapDisabled(e.target); // check if this element, or an ancestor, has `data-tap-disabled` attribute
        },

        isTextInput: function (ele) {
            return !!ele &&
                (ele.tagName == 'TEXTAREA' ||
                ele.contentEditable === 'true' ||
                (ele.tagName == 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset|color|image|button)$/i).test(ele.type)));
        },

        isDateInput: function (ele) {
            return !!ele &&
                (ele.tagName == 'INPUT' && (/^(date|time|datetime-local|month|week)$/i).test(ele.type));
        },

        isVideo: function (ele) {
            return !!ele &&
                (ele.tagName == 'VIDEO');
        },

        isKeyboardElement: function (ele) {
            if (!ionic.Platform.isIOS() || ionic.Platform.isIPad()) {
                return ionic.tap.isTextInput(ele) && !ionic.tap.isDateInput(ele);
            } else {
                return ionic.tap.isTextInput(ele) || ( !!ele && ele.tagName == "SELECT");
            }
        },

        isLabelWithTextInput: function (ele) {
            var container = tapContainingElement(ele, false);

            return !!container &&
                ionic.tap.isTextInput(tapTargetElement(container));
        },

        containsOrIsTextInput: function (ele) {
            return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele);
        },

        cloneFocusedInput: function (container) {
            if (ionic.tap.hasCheckedClone) return;
            ionic.tap.hasCheckedClone = true;

            ionic.requestAnimationFrame(function () {
                var focusInput = container.querySelector(':focus');
                if (ionic.tap.isTextInput(focusInput) && !ionic.tap.isDateInput(focusInput)) {
                    var clonedInput = focusInput.cloneNode(true);

                    clonedInput.value = focusInput.value;
                    clonedInput.classList.add('cloned-text-input');
                    clonedInput.readOnly = true;
                    if (focusInput.isContentEditable) {
                        clonedInput.contentEditable = focusInput.contentEditable;
                        clonedInput.innerHTML = focusInput.innerHTML;
                    }
                    focusInput.parentElement.insertBefore(clonedInput, focusInput);
                    focusInput.classList.add('previous-input-focus');

                    clonedInput.scrollTop = focusInput.scrollTop;
                }
            });
        },

        hasCheckedClone: false,

        removeClonedInputs: function (container) {
            ionic.tap.hasCheckedClone = false;

            ionic.requestAnimationFrame(function () {
                var clonedInputs = container.querySelectorAll('.cloned-text-input');
                var previousInputFocus = container.querySelectorAll('.previous-input-focus');
                var x;

                for (x = 0; x < clonedInputs.length; x++) {
                    clonedInputs[x].parentElement.removeChild(clonedInputs[x]);
                }

                for (x = 0; x < previousInputFocus.length; x++) {
                    previousInputFocus[x].classList.remove('previous-input-focus');
                    previousInputFocus[x].style.top = '';
                    if (ionic.keyboard.isOpen && !ionic.keyboard.isClosing) previousInputFocus[x].focus();
                }
            });
        },

        requiresNativeClick: function (ele) {
            if (ionic.Platform.isWindowsPhone() && (ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click') || (ele.tagName == 'INPUT' && (ele.type == 'button' || ele.type == 'submit')))) {
                return true; //Windows Phone edge case, prevent ng-click (and similar) events from firing twice on this platform
            }
            if (!ele || ele.disabled || (/^(file|range)$/i).test(ele.type) || (/^(object|video)$/i).test(ele.tagName) || ionic.tap.isLabelContainingFileInput(ele)) {
                return true;
            }
            return ionic.tap.isElementTapDisabled(ele);
        },

        isLabelContainingFileInput: function (ele) {
            var lbl = tapContainingElement(ele);
            if (lbl.tagName !== 'LABEL') return false;
            var fileInput = lbl.querySelector('input[type=file]');
            if (fileInput && fileInput.disabled === false) return true;
            return false;
        },

        isElementTapDisabled: function (ele) {
            if (ele && ele.nodeType === 1) {
                var element = ele;
                while (element) {
                    if (element.getAttribute && element.getAttribute('data-tap-disabled') == 'true') {
                        return true;
                    }
                    element = element.parentElement;
                }
            }
            return false;
        },

        setTolerance: function (releaseTolerance, releaseButtonTolerance) {
            TAP_RELEASE_TOLERANCE = releaseTolerance;
            TAP_RELEASE_BUTTON_TOLERANCE = releaseButtonTolerance;
        },

        cancelClick: function () {
            // used to cancel any simulated clicks which may happen on a touchend/mouseup
            // gestures uses this method within its tap and hold events
            tapPointerMoved = true;
        },

        pointerCoord: function (event) {
            // This method can get coordinates for both a mouse click
            // or a touch depending on the given event
            var c = {x: 0, y: 0};
            if (event) {
                var touches = event.touches && event.touches.length ? event.touches : [event];
                var e = (event.changedTouches && event.changedTouches[0]) || touches[0];
                if (e) {
                    c.x = e.clientX || e.pageX || 0;
                    c.y = e.clientY || e.pageY || 0;
                }
            }
            return c;
        }

    };

    function tapEventListener(type, enable, useCapture) {
        if (enable !== false) {
            tapDoc.addEventListener(type, tapEventListeners[type], useCapture);
        } else {
            tapDoc.removeEventListener(type, tapEventListeners[type]);
        }
    }

    function tapClick(e) {
        // simulate a normal click by running the element's click method then focus on it
        var container = tapContainingElement(e.target);
        var ele = tapTargetElement(container);

        if (ionic.tap.requiresNativeClick(ele) || tapPointerMoved) return false;

        var c = ionic.tap.pointerCoord(e);

        //console.log('tapClick', e.type, ele.tagName, '('+c.x+','+c.y+')');
        triggerMouseEvent('click', ele, c.x, c.y);

        // if it's an input, focus in on the target, otherwise blur
        tapHandleFocus(ele);
    }

    function triggerMouseEvent(type, ele, x, y) {
        // using initMouseEvent instead of MouseEvent for our Android friends
        var clickEvent = document.createEvent("MouseEvents");
        clickEvent.initMouseEvent(type, true, true, window, 1, 0, 0, x, y, false, false, false, false, 0, null);
        clickEvent.isIonicTap = true;
        ele.dispatchEvent(clickEvent);
    }

    function tapClickGateKeeper(e) {
        //console.log('click ' + Date.now() + ' isIonicTap: ' + (e.isIonicTap ? true : false));
        if (e.target.type == 'submit' && e.detail === 0) {
            // do not prevent click if it came from an "Enter" or "Go" keypress submit
            return null;
        }

        // do not allow through any click events that were not created by ionic.tap
        if ((ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) ||
            (!e.isIonicTap && !ionic.tap.requiresNativeClick(e.target))) {
            //console.log('clickPrevent', e.target.tagName);
            e.stopPropagation();

            if (!ionic.tap.isLabelWithTextInput(e.target)) {
                // labels clicks from native should not preventDefault othersize keyboard will not show on input focus
                e.preventDefault();
            }
            return false;
        }
    }

// MOUSE
    function tapMouseDown(e) {
        //console.log('mousedown ' + Date.now());
        if (e.isIonicTap || tapIgnoreEvent(e)) return null;

        if (tapEnabledTouchEvents) {
            //console.log('mousedown', 'stop event');
            e.stopPropagation();

            if (!ionic.Platform.isEdge() && (!ionic.tap.isTextInput(e.target) || tapLastTouchTarget !== e.target) && !isSelectOrOption(e.target.tagName) && !ionic.tap.isVideo(e.target)) {
                // If you preventDefault on a text input then you cannot move its text caret/cursor.
                // Allow through only the text input default. However, without preventDefault on an
                // input the 300ms delay can change focus on inputs after the keyboard shows up.
                // The focusin event handles the chance of focus changing after the keyboard shows.
                // Windows Phone - if you preventDefault on a video element then you cannot operate
                // its native controls.
                e.preventDefault();
            }

            return false;
        }

        tapPointerMoved = false;
        tapPointerStart = ionic.tap.pointerCoord(e);

        tapEventListener('mousemove');
        ionic.activator.start(e);
    }

    function tapMouseUp(e) {
        //console.log("mouseup " + Date.now());
        if (tapEnabledTouchEvents) {
            e.stopPropagation();
            e.preventDefault();
            return false;
        }

        if (tapIgnoreEvent(e) || isSelectOrOption(e.target.tagName)) return false;

        if (!tapHasPointerMoved(e)) {
            tapClick(e);
        }
        tapEventListener('mousemove', false);
        ionic.activator.end();
        tapPointerMoved = false;
    }

    function tapMouseMove(e) {
        if (tapHasPointerMoved(e)) {
            tapEventListener('mousemove', false);
            ionic.activator.end();
            tapPointerMoved = true;
            return false;
        }
    }


// TOUCH
    function tapTouchStart(e) {
        //console.log("touchstart " + Date.now());
        if (tapIgnoreEvent(e)) return;

        tapPointerMoved = false;

        tapEnableTouchEvents();
        tapPointerStart = ionic.tap.pointerCoord(e);

        tapEventListener(tapTouchMoveListener);
        ionic.activator.start(e);

        if (ionic.Platform.isIOS() && ionic.tap.isLabelWithTextInput(e.target)) {
            // if the tapped element is a label, which has a child input
            // then preventDefault so iOS doesn't ugly auto scroll to the input
            // but do not prevent default on Android or else you cannot move the text caret
            // and do not prevent default on Android or else no virtual keyboard shows up

            var textInput = tapTargetElement(tapContainingElement(e.target));
            if (textInput !== tapActiveEle) {
                // don't preventDefault on an already focused input or else iOS's text caret isn't usable
                //console.log('Would prevent default here');
                e.preventDefault();
            }
        }
    }

    function tapTouchEnd(e) {
        //console.log('touchend ' + Date.now());
        if (tapIgnoreEvent(e)) return;

        tapEnableTouchEvents();
        if (!tapHasPointerMoved(e)) {
            tapClick(e);

            if (isSelectOrOption(e.target.tagName)) {
                e.preventDefault();
            }
        }

        tapLastTouchTarget = e.target;
        tapTouchCancel();
    }

    function tapTouchMove(e) {
        if (tapHasPointerMoved(e)) {
            tapPointerMoved = true;
            tapEventListener(tapTouchMoveListener, false);
            ionic.activator.end();
            return false;
        }
    }

    function tapTouchCancel() {
        tapEventListener(tapTouchMoveListener, false);
        ionic.activator.end();
        tapPointerMoved = false;
    }

    function tapEnableTouchEvents() {
        tapEnabledTouchEvents = true;
        clearTimeout(tapMouseResetTimer);
        tapMouseResetTimer = setTimeout(function () {
            tapEnabledTouchEvents = false;
        }, 600);
    }

    function tapIgnoreEvent(e) {
        if (e.isTapHandled) return true;
        e.isTapHandled = true;

        if (ionic.tap.isElementTapDisabled(e.target)) {
            return true;
        }

        if (e.target.tagName == 'SELECT') {
            return true;
        }

        if (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) {
            e.preventDefault();
            return true;
        }
    }

    function tapHandleFocus(ele) {
        tapTouchFocusedInput = null;

        var triggerFocusIn = false;

        if (ele.tagName == 'SELECT') {
            // trick to force Android options to show up
            triggerMouseEvent('mousedown', ele, 0, 0);
            ele.focus && ele.focus();
            triggerFocusIn = true;

        } else if (tapActiveElement() === ele) {
            // already is the active element and has focus
            triggerFocusIn = true;

        } else if ((/^(input|textarea|ion-label)$/i).test(ele.tagName) || ele.isContentEditable) {
            triggerFocusIn = true;
            ele.focus && ele.focus();
            ele.value = ele.value;
            if (tapEnabledTouchEvents) {
                tapTouchFocusedInput = ele;
            }

        } else {
            tapFocusOutActive();
        }

        if (triggerFocusIn) {
            tapActiveElement(ele);
            ionic.trigger('ionic.focusin', {
                target: ele
            }, true);
        }
    }

    function tapFocusOutActive() {
        var ele = tapActiveElement();
        if (ele && ((/^(input|textarea|select)$/i).test(ele.tagName) || ele.isContentEditable)) {
            //console.log('tapFocusOutActive', ele.tagName);
            ele.blur();
        }
        tapActiveElement(null);
    }

    function tapFocusIn(e) {
        //console.log('focusin ' + Date.now());
        // Because a text input doesn't preventDefault (so the caret still works) there's a chance
        // that its mousedown event 300ms later will change the focus to another element after
        // the keyboard shows up.

        if (tapEnabledTouchEvents &&
            ionic.tap.isTextInput(tapActiveElement()) &&
            ionic.tap.isTextInput(tapTouchFocusedInput) &&
            tapTouchFocusedInput !== e.target) {

            // 1) The pointer is from touch events
            // 2) There is an active element which is a text input
            // 3) A text input was just set to be focused on by a touch event
            // 4) A new focus has been set, however the target isn't the one the touch event wanted
            //console.log('focusin', 'tapTouchFocusedInput');
            tapTouchFocusedInput.focus();
            tapTouchFocusedInput = null;
        }
        ionic.scroll.isScrolling = false;
    }

    function tapFocusOut() {
        //console.log("focusout");
        tapActiveElement(null);
    }

    function tapActiveElement(ele) {
        if (arguments.length) {
            tapActiveEle = ele;
        }
        return tapActiveEle || document.activeElement;
    }

    function tapHasPointerMoved(endEvent) {
        if (!endEvent || endEvent.target.nodeType !== 1 || !tapPointerStart || (tapPointerStart.x === 0 && tapPointerStart.y === 0)) {
            return false;
        }
        var endCoordinates = ionic.tap.pointerCoord(endEvent);

        var hasClassList = !!(endEvent.target.classList && endEvent.target.classList.contains &&
        typeof endEvent.target.classList.contains === 'function');
        var releaseTolerance = hasClassList && endEvent.target.classList.contains('button') ?
            TAP_RELEASE_BUTTON_TOLERANCE :
            TAP_RELEASE_TOLERANCE;

        return Math.abs(tapPointerStart.x - endCoordinates.x) > releaseTolerance ||
            Math.abs(tapPointerStart.y - endCoordinates.y) > releaseTolerance;
    }

    function tapContainingElement(ele, allowSelf) {
        var climbEle = ele;
        for (var x = 0; x < 6; x++) {
            if (!climbEle) break;
            if (climbEle.tagName === 'LABEL') return climbEle;
            climbEle = climbEle.parentElement;
        }
        if (allowSelf !== false) return ele;
    }

    function tapTargetElement(ele) {
        if (ele && ele.tagName === 'LABEL') {
            if (ele.control) return ele.control;

            // older devices do not support the "control" property
            if (ele.querySelector) {
                var control = ele.querySelector('input,textarea,select');
                if (control) return control;
            }
        }
        return ele;
    }

    function isSelectOrOption(tagName) {
        return (/^(select|option)$/i).test(tagName);
    }

    ionic.DomUtil.ready(function () {
        var ng = typeof angular !== 'undefined' ? angular : null;
        //do nothing for e2e tests
        if (!ng || (ng && !ng.scenario)) {
            ionic.tap.register(document);
        }
    });

    (function (document, ionic) {
        'use strict';

        var queueElements = {};   // elements that should get an active state in XX milliseconds
        var activeElements = {};  // elements that are currently active
        var keyId = 0;            // a counter for unique keys for the above ojects
        var ACTIVATED_CLASS = 'activated';

        ionic.activator = {

            start: function (e) {
                var hitX = ionic.tap.pointerCoord(e).x;
                if (hitX > 0 && hitX < 30) {
                    return;
                }

                // when an element is touched/clicked, it climbs up a few
                // parents to see if it is an .item or .button element
                ionic.requestAnimationFrame(function () {
                    if ((ionic.scroll && ionic.scroll.isScrolling) || ionic.tap.requiresNativeClick(e.target)) return;
                    var ele = e.target;
                    var eleToActivate;

                    for (var x = 0; x < 6; x++) {
                        if (!ele || ele.nodeType !== 1) break;
                        if (eleToActivate && ele.classList && ele.classList.contains('item')) {
                            eleToActivate = ele;
                            break;
                        }
                        if (ele.tagName == 'A' || ele.tagName == 'BUTTON' || ele.hasAttribute('ng-click')) {
                            eleToActivate = ele;
                            break;
                        }
                        if (ele.classList && ele.classList.contains('button')) {
                            eleToActivate = ele;
                            break;
                        }
                        // no sense climbing past these
                        if (ele.tagName == 'ION-CONTENT' || (ele.classList && ele.classList.contains('pane')) || ele.tagName == 'BODY') {
                            break;
                        }
                        ele = ele.parentElement;
                    }

                    if (eleToActivate) {
                        // queue that this element should be set to active
                        queueElements[keyId] = eleToActivate;

                        // on the next frame, set the queued elements to active
                        ionic.requestAnimationFrame(activateElements);

                        keyId = (keyId > 29 ? 0 : keyId + 1);
                    }

                });
            },

            end: function () {
                // clear out any active/queued elements after XX milliseconds
                setTimeout(clear, 200);
            }

        };

        function clear() {
            // clear out any elements that are queued to be set to active
            queueElements = {};

            // in the next frame, remove the active class from all active elements
            ionic.requestAnimationFrame(deactivateElements);
        }

        function activateElements() {
            // activate all elements in the queue
            for (var key in queueElements) {
                if (queueElements[key]) {
                    queueElements[key].classList.add(ACTIVATED_CLASS);
                    activeElements[key] = queueElements[key];
                }
            }
            queueElements = {};
        }

        function deactivateElements() {
            if (ionic.transition && ionic.transition.isActive) {
                setTimeout(deactivateElements, 400);
                return;
            }

            for (var key in activeElements) {
                if (activeElements[key]) {
                    activeElements[key].classList.remove(ACTIVATED_CLASS);
                    delete activeElements[key];
                }
            }
        }

    })(document, ionic);

    (function (ionic) {
        /* for nextUid function below */
        var nextId = 0;

        /**
         * Various utilities used throughout Ionic
         *
         * Some of these are adopted from underscore.js and backbone.js, both also MIT licensed.
         */
        ionic.Utils = {

            arrayMove: function (arr, oldIndex, newIndex) {
                if (newIndex >= arr.length) {
                    var k = newIndex - arr.length;
                    while ((k--) + 1) {
                        arr.push(undefined);
                    }
                }
                arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]);
                return arr;
            },

            /**
             * Return a function that will be called with the given context
             */
            proxy: function (func, context) {
                var args = Array.prototype.slice.call(arguments, 2);
                return function () {
                    return func.apply(context, args.concat(Array.prototype.slice.call(arguments)));
                };
            },

            /**
             * Only call a function once in the given interval.
             *
             * @param func {Function} the function to call
             * @param wait {int} how long to wait before/after to allow function calls
             * @param immediate {boolean} whether to call immediately or after the wait interval
             */
            debounce: function (func, wait, immediate) {
                var timeout, args, context, timestamp, result;
                return function () {
                    context = this;
                    args = arguments;
                    timestamp = new Date();
                    var later = function () {
                        var last = (new Date()) - timestamp;
                        if (last < wait) {
                            timeout = setTimeout(later, wait - last);
                        } else {
                            timeout = null;
                            if (!immediate) result = func.apply(context, args);
                        }
                    };
                    var callNow = immediate && !timeout;
                    if (!timeout) {
                        timeout = setTimeout(later, wait);
                    }
                    if (callNow) result = func.apply(context, args);
                    return result;
                };
            },

            /**
             * Throttle the given fun, only allowing it to be
             * called at most every `wait` ms.
             */
            throttle: function (func, wait, options) {
                var context, args, result;
                var timeout = null;
                var previous = 0;
                options || (options = {});
                var later = function () {
                    previous = options.leading === false ? 0 : Date.now();
                    timeout = null;
                    result = func.apply(context, args);
                };
                return function () {
                    var now = Date.now();
                    if (!previous && options.leading === false) previous = now;
                    var remaining = wait - (now - previous);
                    context = this;
                    args = arguments;
                    if (remaining <= 0) {
                        clearTimeout(timeout);
                        timeout = null;
                        previous = now;
                        result = func.apply(context, args);
                    } else if (!timeout && options.trailing !== false) {
                        timeout = setTimeout(later, remaining);
                    }
                    return result;
                };
            },
            // Borrowed from Backbone.js's extend
            // Helper function to correctly set up the prototype chain, for subclasses.
            // Similar to `goog.inherits`, but uses a hash of prototype properties and
            // class properties to be extended.
            inherit: function (protoProps, staticProps) {
                var parent = this;
                var child;

                // The constructor function for the new subclass is either defined by you
                // (the "constructor" property in your `extend` definition), or defaulted
                // by us to simply call the parent's constructor.
                if (protoProps && protoProps.hasOwnProperty('constructor')) {
                    child = protoProps.constructor;
                } else {
                    child = function () {
                        return parent.apply(this, arguments);
                    };
                }

                // Add static properties to the constructor function, if supplied.
                ionic.extend(child, parent, staticProps);

                // Set the prototype chain to inherit from `parent`, without calling
                // `parent`'s constructor function.
                var Surrogate = function () {
                    this.constructor = child;
                };
                Surrogate.prototype = parent.prototype;
                child.prototype = new Surrogate();

                // Add prototype properties (instance properties) to the subclass,
                // if supplied.
                if (protoProps) ionic.extend(child.prototype, protoProps);

                // Set a convenience property in case the parent's prototype is needed
                // later.
                child.__super__ = parent.prototype;

                return child;
            },

            // Extend adapted from Underscore.js
            extend: function (obj) {
                var args = Array.prototype.slice.call(arguments, 1);
                for (var i = 0; i < args.length; i++) {
                    var source = args[i];
                    if (source) {
                        for (var prop in source) {
                            obj[prop] = source[prop];
                        }
                    }
                }
                return obj;
            },

            nextUid: function () {
                return 'ion' + (nextId++);
            },

            disconnectScope: function disconnectScope(scope) {
                if (!scope) return;

                if (scope.$root === scope) {
                    return; // we can't disconnect the root node;
                }
                var parent = scope.$parent;
                scope.$$disconnected = true;
                scope.$broadcast('$ionic.disconnectScope', scope);

                // See Scope.$destroy
                if (parent.$$childHead === scope) {
                    parent.$$childHead = scope.$$nextSibling;
                }
                if (parent.$$childTail === scope) {
                    parent.$$childTail = scope.$$prevSibling;
                }
                if (scope.$$prevSibling) {
                    scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
                }
                if (scope.$$nextSibling) {
                    scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
                }
                scope.$$nextSibling = scope.$$prevSibling = null;
            },

            reconnectScope: function reconnectScope(scope) {
                if (!scope) return;

                if (scope.$root === scope) {
                    return; // we can't disconnect the root node;
                }
                if (!scope.$$disconnected) {
                    return;
                }
                var parent = scope.$parent;
                scope.$$disconnected = false;
                scope.$broadcast('$ionic.reconnectScope', scope);
                // See Scope.$new for this logic...
                scope.$$prevSibling = parent.$$childTail;
                if (parent.$$childHead) {
                    parent.$$childTail.$$nextSibling = scope;
                    parent.$$childTail = scope;
                } else {
                    parent.$$childHead = parent.$$childTail = scope;
                }
            },

            isScopeDisconnected: function (scope) {
                var climbScope = scope;
                while (climbScope) {
                    if (climbScope.$$disconnected) return true;
                    climbScope = climbScope.$parent;
                }
                return false;
            }
        };

        // Bind a few of the most useful functions to the ionic scope
        ionic.inherit = ionic.Utils.inherit;
        ionic.extend = ionic.Utils.extend;
        ionic.throttle = ionic.Utils.throttle;
        ionic.proxy = ionic.Utils.proxy;
        ionic.debounce = ionic.Utils.debounce;

    })(window.ionic);

    /**
     * @ngdoc page
     * @name keyboard
     * @module ionic
     * @description
     * On both Android and iOS, Ionic will attempt to prevent the keyboard from
     * obscuring inputs and focusable elements when it appears by scrolling them
     * into view.  In order for this to work, any focusable elements must be within
     * a [Scroll View](http://ionicframework.com/docs/api/directive/ionScroll/)
     * or a directive such as [Content](http://ionicframework.com/docs/api/directive/ionContent/)
     * that has a Scroll View.
     *
     * It will also attempt to prevent the native overflow scrolling on focus,
     * which can cause layout issues such as pushing headers up and out of view.
     *
     * The keyboard fixes work best in conjunction with the
     * [Ionic Keyboard Plugin](https://github.com/driftyco/ionic-plugins-keyboard),
     * although it will perform reasonably well without.  However, if you are using
     * Cordova there is no reason not to use the plugin.
     *
     * ### Hide when keyboard shows
     *
     * To hide an element when the keyboard is open, add the class `hide-on-keyboard-open`.
     *
     * ```html
     * <div class="hide-on-keyboard-open">
     *   <div id="google-map"></div>
     * </div>
     * ```
     *
     * Note: For performance reasons, elements will not be hidden for 400ms after the start of the `native.keyboardshow` event
     * from the Ionic Keyboard plugin. If you would like them to disappear immediately, you could do something
     * like:
     *
     * ```js
     *   window.addEventListener('native.keyboardshow', function(){
 *     document.body.classList.add('keyboard-open');
 *   });
     * ```
     * This adds the same `keyboard-open` class that is normally added by Ionic 400ms after the keyboard
     * opens. However, bear in mind that adding this class to the body immediately may cause jank in any
     * animations on Android that occur when the keyboard opens (for example, scrolling any obscured inputs into view).
     *
     * ----------
     *
     * ### Plugin Usage
     * Information on using the plugin can be found at
     * [https://github.com/driftyco/ionic-plugins-keyboard](https://github.com/driftyco/ionic-plugins-keyboard).
     *
     * ----------
     *
     * ### Android Notes
     * - If your app is running in fullscreen, i.e. you have
     *   `<preference name="Fullscreen" value="true" />` in your `config.xml` file
     *   you will need to set `ionic.Platform.isFullScreen = true` manually.
     *
     * - You can configure the behavior of the web view when the keyboard shows by setting
     *   [android:windowSoftInputMode](http://developer.android.com/reference/android/R.attr.html#windowSoftInputMode)
     *   to either `adjustPan`, `adjustResize` or `adjustNothing` in your app's
     *   activity in `AndroidManifest.xml`. `adjustResize` is the recommended setting
     *   for Ionic, but if for some reason you do use `adjustPan` you will need to
     *   set `ionic.Platform.isFullScreen = true`.
     *
     *   ```xml
     *   <activity android:windowSoftInputMode="adjustResize">
     *
     *   ```
     *
     * ### iOS Notes
     * - If the content of your app (including the header) is being pushed up and
     *   out of view on input focus, try setting `cordova.plugins.Keyboard.disableScroll(true)`.
     *   This does **not** disable scrolling in the Ionic scroll view, rather it
     *   disables the native overflow scrolling that happens automatically as a
     *   result of focusing on inputs below the keyboard.
     *
     */

    /**
     * The current viewport height.
     */
    var keyboardCurrentViewportHeight = 0;

    /**
     * The viewport height when in portrait orientation.
     */
    var keyboardPortraitViewportHeight = 0;

    /**
     * The viewport height when in landscape orientation.
     */
    var keyboardLandscapeViewportHeight = 0;

    /**
     * The currently focused input.
     */
    var keyboardActiveElement;

    /**
     * The previously focused input used to reset keyboard after focusing on a
     * new non-keyboard element
     */
    var lastKeyboardActiveElement;

    /**
     * The scroll view containing the currently focused input.
     */
    var scrollView;

    /**
     * Timer for the setInterval that polls window.innerHeight to determine whether
     * the layout has updated for the keyboard showing/hiding.
     */
    var waitForResizeTimer;

    /**
     * Sometimes when switching inputs or orientations, focusout will fire before
     * focusin, so this timer is for the small setTimeout to determine if we should
     * really focusout/hide the keyboard.
     */
    var keyboardFocusOutTimer;

    /**
     * on Android, orientationchange will fire before the keyboard plugin notifies
     * the browser that the keyboard will show/is showing, so this flag indicates
     * to nativeShow that there was an orientationChange and we should update
     * the viewport height with an accurate keyboard height value
     */
    var wasOrientationChange = false;

    /**
     * CSS class added to the body indicating the keyboard is open.
     */
    var KEYBOARD_OPEN_CSS = 'keyboard-open';

    /**
     * CSS class that indicates a scroll container.
     */
    var SCROLL_CONTAINER_CSS = 'scroll-content';

    /**
     * Debounced keyboardFocusIn function
     */
    var debouncedKeyboardFocusIn = ionic.debounce(keyboardFocusIn, 200, true);

    /**
     * Debounced keyboardNativeShow function
     */
    var debouncedKeyboardNativeShow = ionic.debounce(keyboardNativeShow, 100, true);

    /**
     * Ionic keyboard namespace.
     * @namespace keyboard
     */
    ionic.keyboard = {

        /**
         * Whether the keyboard is open or not.
         */
        isOpen: false,

        /**
         * Whether the keyboard is closing or not.
         */
        isClosing: false,

        /**
         * Whether the keyboard is opening or not.
         */
        isOpening: false,

        /**
         * The height of the keyboard in pixels, as reported by the keyboard plugin.
         * If the plugin is not available, calculated as the difference in
         * window.innerHeight after the keyboard has shown.
         */
        height: 0,

        /**
         * Whether the device is in landscape orientation or not.
         */
        isLandscape: false,

        /**
         * Whether the keyboard event listeners have been added or not
         */
        isInitialized: false,

        /**
         * Hide the keyboard, if it is open.
         */
        hide: function () {
            if (keyboardHasPlugin()) {
                cordova.plugins.Keyboard.close();
            }
            keyboardActiveElement && keyboardActiveElement.blur();
        },

        /**
         * An alias for cordova.plugins.Keyboard.show(). If the keyboard plugin
         * is installed, show the keyboard.
         */
        show: function () {
            if (keyboardHasPlugin()) {
                cordova.plugins.Keyboard.show();
            }
        },

        /**
         * Remove all keyboard related event listeners, effectively disabling Ionic's
         * keyboard adjustments.
         */
        disable: function () {
            if (keyboardHasPlugin()) {
                window.removeEventListener('native.keyboardshow', debouncedKeyboardNativeShow);
                window.removeEventListener('native.keyboardhide', keyboardFocusOut);
            } else {
                document.body.removeEventListener('focusout', keyboardFocusOut);
            }

            document.body.removeEventListener('ionic.focusin', debouncedKeyboardFocusIn);
            document.body.removeEventListener('focusin', debouncedKeyboardFocusIn);

            window.removeEventListener('orientationchange', keyboardOrientationChange);

            if (window.navigator.msPointerEnabled) {
                document.removeEventListener("MSPointerDown", keyboardInit);
            } else {
                document.removeEventListener('touchstart', keyboardInit);
            }
            ionic.keyboard.isInitialized = false;
        },

        /**
         * Alias for keyboardInit, initialize all keyboard related event listeners.
         */
        enable: function () {
            keyboardInit();
        }
    };

// Initialize the viewport height (after ionic.keyboard.height has been
// defined).
    keyboardCurrentViewportHeight = getViewportHeight();


    /* Event handlers */
    /* ------------------------------------------------------------------------- */

    /**
     * Event handler for first touch event, initializes all event listeners
     * for keyboard related events. Also aliased by ionic.keyboard.enable.
     */
    function keyboardInit() {

        if (ionic.keyboard.isInitialized) return;

        if (keyboardHasPlugin()) {
            window.addEventListener('native.keyboardshow', debouncedKeyboardNativeShow);
            window.addEventListener('native.keyboardhide', keyboardFocusOut);
        } else {
            document.body.addEventListener('focusout', keyboardFocusOut);
        }

        document.body.addEventListener('ionic.focusin', debouncedKeyboardFocusIn);
        document.body.addEventListener('focusin', debouncedKeyboardFocusIn);

        if (window.navigator.msPointerEnabled) {
            document.removeEventListener("MSPointerDown", keyboardInit);
        } else {
            document.removeEventListener('touchstart', keyboardInit);
        }

        ionic.keyboard.isInitialized = true;
    }

    /**
     * Event handler for 'native.keyboardshow' event, sets keyboard.height to the
     * reported height and keyboard.isOpening to true. Then calls
     * keyboardWaitForResize with keyboardShow or keyboardUpdateViewportHeight as
     * the callback depending on whether the event was triggered by a focusin or
     * an orientationchange.
     */
    function keyboardNativeShow(e) {
        clearTimeout(keyboardFocusOutTimer);
        //console.log("keyboardNativeShow fired at: " + Date.now());
        //console.log("keyboardNativeshow window.innerHeight: " + window.innerHeight);

        if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
            ionic.keyboard.isOpening = true;
            ionic.keyboard.isClosing = false;
        }

        ionic.keyboard.height = e.keyboardHeight;
        //console.log('nativeshow keyboard height:' + e.keyboardHeight);

        if (wasOrientationChange) {
            keyboardWaitForResize(keyboardUpdateViewportHeight, true);
        } else {
            keyboardWaitForResize(keyboardShow, true);
        }
    }

    /**
     * Event handler for 'focusin' and 'ionic.focusin' events. Initializes
     * keyboard state (keyboardActiveElement and keyboard.isOpening) for the
     * appropriate adjustments once the window has resized.  If not using the
     * keyboard plugin, calls keyboardWaitForResize with keyboardShow as the
     * callback or keyboardShow right away if the keyboard is already open.  If
     * using the keyboard plugin does nothing and lets keyboardNativeShow handle
     * adjustments with a more accurate keyboard height.
     */
    function keyboardFocusIn(e) {
        clearTimeout(keyboardFocusOutTimer);
        //console.log("keyboardFocusIn from: " + e.type + " at: " + Date.now());

        if (!e.target ||
            e.target.readOnly || !ionic.tap.isKeyboardElement(e.target) || !(scrollView = ionic.DomUtil.getParentWithClass(e.target, SCROLL_CONTAINER_CSS))) {
            if (keyboardActiveElement) {
                lastKeyboardActiveElement = keyboardActiveElement;
            }
            keyboardActiveElement = null;
            return;
        }

        keyboardActiveElement = e.target;

        // if using JS scrolling, undo the effects of native overflow scroll so the
        // scroll view is positioned correctly
        if (!scrollView.classList.contains("overflow-scroll")) {
            document.body.scrollTop = 0;
            scrollView.scrollTop = 0;
            ionic.requestAnimationFrame(function () {
                document.body.scrollTop = 0;
                scrollView.scrollTop = 0;
            });

            // any showing part of the document that isn't within the scroll the user
            // could touchmove and cause some ugly changes to the app, so disable
            // any touchmove events while the keyboard is open using e.preventDefault()
            if (window.navigator.msPointerEnabled) {
                document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
            } else {
                document.addEventListener('touchmove', keyboardPreventDefault, false);
            }
        }

        if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
            ionic.keyboard.isOpening = true;
            ionic.keyboard.isClosing = false;
        }

        // attempt to prevent browser from natively scrolling input into view while
        // we are trying to do the same (while we are scrolling) if the user taps the
        // keyboard
        document.addEventListener('keydown', keyboardOnKeyDown, false);


        // if we aren't using the plugin and the keyboard isn't open yet, wait for the
        // window to resize so we can get an accurate estimate of the keyboard size,
        // otherwise we do nothing and let nativeShow call keyboardShow once we have
        // an exact keyboard height
        // if the keyboard is already open, go ahead and scroll the input into view
        // if necessary
        if (!ionic.keyboard.isOpen && !keyboardHasPlugin()) {
            keyboardWaitForResize(keyboardShow, true);

        } else if (ionic.keyboard.isOpen) {
            keyboardShow();
        }
    }

    /**
     * Event handler for 'focusout' events. Sets keyboard.isClosing to true and
     * calls keyboardWaitForResize with keyboardHide as the callback after a small
     * timeout.
     */
    function keyboardFocusOut() {
        clearTimeout(keyboardFocusOutTimer);
        //console.log("keyboardFocusOut fired at: " + Date.now());
        //console.log("keyboardFocusOut event type: " + e.type);

        if (ionic.keyboard.isOpen || ionic.keyboard.isOpening) {
            ionic.keyboard.isClosing = true;
            ionic.keyboard.isOpening = false;
        }

        // Call keyboardHide with a slight delay because sometimes on focus or
        // orientation change focusin is called immediately after, so we give it time
        // to cancel keyboardHide
        keyboardFocusOutTimer = setTimeout(function () {
            ionic.requestAnimationFrame(function () {
                // focusOut during or right after an orientationchange, so we didn't get
                // a chance to update the viewport height yet, do it and keyboardHide
                //console.log("focusOut, wasOrientationChange: " + wasOrientationChange);
                if (wasOrientationChange) {
                    keyboardWaitForResize(function () {
                        keyboardUpdateViewportHeight();
                        keyboardHide();
                    }, false);
                } else {
                    keyboardWaitForResize(keyboardHide, false);
                }
            });
        }, 50);
    }

    /**
     * Event handler for 'orientationchange' events. If using the keyboard plugin
     * and the keyboard is open on Android, sets wasOrientationChange to true so
     * nativeShow can update the viewport height with an accurate keyboard height.
     * If the keyboard isn't open or keyboard plugin isn't being used,
     * waits for the window to resize before updating the viewport height.
     *
     * On iOS, where orientationchange fires after the keyboard has already shown,
     * updates the viewport immediately, regardless of if the keyboard is already
     * open.
     */
    function keyboardOrientationChange() {
        //console.log("orientationchange fired at: " + Date.now());
        //console.log("orientation was: " + (ionic.keyboard.isLandscape ? "landscape" : "portrait"));

        // toggle orientation
        ionic.keyboard.isLandscape = !ionic.keyboard.isLandscape;
        // //console.log("now orientation is: " + (ionic.keyboard.isLandscape ? "landscape" : "portrait"));

        // no need to wait for resizing on iOS, and orientationchange always fires
        // after the keyboard has opened, so it doesn't matter if it's open or not
        if (ionic.Platform.isIOS()) {
            keyboardUpdateViewportHeight();
        }

        // On Android, if the keyboard isn't open or we aren't using the keyboard
        // plugin, update the viewport height once everything has resized. If the
        // keyboard is open and we are using the keyboard plugin do nothing and let
        // nativeShow handle it using an accurate keyboard height.
        if (ionic.Platform.isAndroid()) {
            if (!ionic.keyboard.isOpen || !keyboardHasPlugin()) {
                keyboardWaitForResize(keyboardUpdateViewportHeight, false);
            } else {
                wasOrientationChange = true;
            }
        }
    }

    /**
     * Event handler for 'keydown' event. Tries to prevent browser from natively
     * scrolling an input into view when a user taps the keyboard while we are
     * scrolling the input into view ourselves with JS.
     */
    function keyboardOnKeyDown(e) {
        if (ionic.scroll.isScrolling) {
            keyboardPreventDefault(e);
        }
    }

    /**
     * Event for 'touchmove' or 'MSPointerMove'. Prevents native scrolling on
     * elements outside the scroll view while the keyboard is open.
     */
    function keyboardPreventDefault(e) {
        if (e.target.tagName !== 'TEXTAREA') {
            e.preventDefault();
        }
    }

    /* Private API */
    /* -------------------------------------------------------------------------- */

    /**
     * Polls window.innerHeight until it has updated to an expected value (or
     * sufficient time has passed) before calling the specified callback function.
     * Only necessary for non-fullscreen Android which sometimes reports multiple
     * window.innerHeight values during interim layouts while it is resizing.
     *
     * On iOS, the window.innerHeight will already be updated, but we use the 50ms
     * delay as essentially a timeout so that scroll view adjustments happen after
     * the keyboard has shown so there isn't a white flash from us resizing too
     * quickly.
     *
     * @param {Function} callback the function to call once the window has resized
     * @param {boolean} isOpening whether the resize is from the keyboard opening
     * or not
     */
    function keyboardWaitForResize(callback, isOpening) {
        clearInterval(waitForResizeTimer);
        var count = 0;
        var maxCount;
        var initialHeight = getViewportHeight();
        var viewportHeight = initialHeight;

        //console.log("waitForResize initial viewport height: " + viewportHeight);
        //var start = Date.now();
        //console.log("start: " + start);

        // want to fail relatively quickly on modern android devices, since it's much
        // more likely we just have a bad keyboard height
        if (ionic.Platform.isAndroid() && ionic.Platform.version() < 4.4) {
            maxCount = 30;
        } else if (ionic.Platform.isAndroid()) {
            maxCount = 10;
        } else {
            maxCount = 1;
        }

        // poll timer
        waitForResizeTimer = setInterval(function () {
            viewportHeight = getViewportHeight();

            // height hasn't updated yet, try again in 50ms
            // if not using plugin, wait for maxCount to ensure we have waited long enough
            // to get an accurate keyboard height
            if (++count < maxCount &&
                ((!isPortraitViewportHeight(viewportHeight) && !isLandscapeViewportHeight(viewportHeight)) || !ionic.keyboard.height)) {
                return;
            }

            // infer the keyboard height from the resize if not using the keyboard plugin
            if (!keyboardHasPlugin()) {
                ionic.keyboard.height = Math.abs(initialHeight - window.innerHeight);
            }

            // set to true if we were waiting for the keyboard to open
            ionic.keyboard.isOpen = isOpening;

            clearInterval(waitForResizeTimer);
            //var end = Date.now();
            //console.log("waitForResize count: " + count);
            //console.log("end: " + end);
            //console.log("difference: " + ( end - start ) + "ms");

            //console.log("callback: " + callback.name);
            callback();

        }, 50);

        return maxCount; //for tests
    }

    /**
     * On keyboard close sets keyboard state to closed, resets the scroll view,
     * removes CSS from body indicating keyboard was open, removes any event
     * listeners for when the keyboard is open and on Android blurs the active
     * element (which in some cases will still have focus even if the keyboard
     * is closed and can cause it to reappear on subsequent taps).
     */
    function keyboardHide() {
        clearTimeout(keyboardFocusOutTimer);
        //console.log("keyboardHide");

        ionic.keyboard.isOpen = false;
        ionic.keyboard.isClosing = false;

        if (keyboardActiveElement || lastKeyboardActiveElement) {
            ionic.trigger('resetScrollView', {
                target: keyboardActiveElement || lastKeyboardActiveElement
            }, true);
        }

        ionic.requestAnimationFrame(function () {
            document.body.classList.remove(KEYBOARD_OPEN_CSS);
        });

        // the keyboard is gone now, remove the touchmove that disables native scroll
        if (window.navigator.msPointerEnabled) {
            document.removeEventListener("MSPointerMove", keyboardPreventDefault);
        } else {
            document.removeEventListener('touchmove', keyboardPreventDefault);
        }
        document.removeEventListener('keydown', keyboardOnKeyDown);

        if (ionic.Platform.isAndroid()) {
            // on android closing the keyboard with the back/dismiss button won't remove
            // focus and keyboard can re-appear on subsequent taps (like scrolling)
            if (keyboardHasPlugin()) cordova.plugins.Keyboard.close();
            keyboardActiveElement && keyboardActiveElement.blur();
        }

        keyboardActiveElement = null;
        lastKeyboardActiveElement = null;
    }

    /**
     * On keyboard open sets keyboard state to open, adds CSS to the body
     * indicating the keyboard is open and tells the scroll view to resize and
     * the currently focused input into view if necessary.
     */
    function keyboardShow() {

        ionic.keyboard.isOpen = true;
        ionic.keyboard.isOpening = false;

        var details = {
            keyboardHeight: keyboardGetHeight(),
            viewportHeight: keyboardCurrentViewportHeight
        };

        if (keyboardActiveElement) {
            details.target = keyboardActiveElement;

            var elementBounds = keyboardActiveElement.getBoundingClientRect();

            details.elementTop = Math.round(elementBounds.top);
            details.elementBottom = Math.round(elementBounds.bottom);

            details.windowHeight = details.viewportHeight - details.keyboardHeight;
            //console.log("keyboardShow viewportHeight: " + details.viewportHeight +
            //", windowHeight: " + details.windowHeight +
            //", keyboardHeight: " + details.keyboardHeight);

            // figure out if the element is under the keyboard
            details.isElementUnderKeyboard = (details.elementBottom > details.windowHeight);
            //console.log("isUnderKeyboard: " + details.isElementUnderKeyboard);
            //console.log("elementBottom: " + details.elementBottom);

            // send event so the scroll view adjusts
            ionic.trigger('scrollChildIntoView', details, true);
        }

        setTimeout(function () {
            document.body.classList.add(KEYBOARD_OPEN_CSS);
        }, 400);

        return details; //for testing
    }

    /* eslint no-unused-vars:0 */
    function keyboardGetHeight() {
        // check if we already have a keyboard height from the plugin or resize calculations
        if (ionic.keyboard.height) {
            return ionic.keyboard.height;
        }

        if (ionic.Platform.isAndroid()) {
            // should be using the plugin, no way to know how big the keyboard is, so guess
            if (ionic.Platform.isFullScreen) {
                return 275;
            }
            // otherwise just calculate it
            var contentHeight = window.innerHeight;
            if (contentHeight < keyboardCurrentViewportHeight) {
                return keyboardCurrentViewportHeight - contentHeight;
            } else {
                return 0;
            }
        }

        // fallback for when it's the webview without the plugin
        // or for just the standard web browser
        // TODO: have these be based on device
        if (ionic.Platform.isIOS()) {
            if (ionic.keyboard.isLandscape) {
                return 206;
            }

            if (!ionic.Platform.isWebView()) {
                return 216;
            }

            return 260;
        }

        // safe guess
        return 275;
    }

    function isPortraitViewportHeight(viewportHeight) {
        return !!(!ionic.keyboard.isLandscape &&
        keyboardPortraitViewportHeight &&
        (Math.abs(keyboardPortraitViewportHeight - viewportHeight) < 2));
    }

    function isLandscapeViewportHeight(viewportHeight) {
        return !!(ionic.keyboard.isLandscape &&
        keyboardLandscapeViewportHeight &&
        (Math.abs(keyboardLandscapeViewportHeight - viewportHeight) < 2));
    }

    function keyboardUpdateViewportHeight() {
        wasOrientationChange = false;
        keyboardCurrentViewportHeight = getViewportHeight();

        if (ionic.keyboard.isLandscape && !keyboardLandscapeViewportHeight) {
            //console.log("saved landscape: " + keyboardCurrentViewportHeight);
            keyboardLandscapeViewportHeight = keyboardCurrentViewportHeight;

        } else if (!ionic.keyboard.isLandscape && !keyboardPortraitViewportHeight) {
            //console.log("saved portrait: " + keyboardCurrentViewportHeight);
            keyboardPortraitViewportHeight = keyboardCurrentViewportHeight;
        }

        if (keyboardActiveElement) {
            ionic.trigger('resetScrollView', {
                target: keyboardActiveElement
            }, true);
        }

        if (ionic.keyboard.isOpen && ionic.tap.isTextInput(keyboardActiveElement)) {
            keyboardShow();
        }
    }

    function keyboardInitViewportHeight() {
        var viewportHeight = getViewportHeight();
        //console.log("Keyboard init VP: " + viewportHeight + " " + window.innerWidth);
        // can't just use window.innerHeight in case the keyboard is opened immediately
        if ((viewportHeight / window.innerWidth) < 1) {
            ionic.keyboard.isLandscape = true;
        }
        //console.log("ionic.keyboard.isLandscape is: " + ionic.keyboard.isLandscape);

        // initialize or update the current viewport height values
        keyboardCurrentViewportHeight = viewportHeight;
        if (ionic.keyboard.isLandscape && !keyboardLandscapeViewportHeight) {
            keyboardLandscapeViewportHeight = keyboardCurrentViewportHeight;
        } else if (!ionic.keyboard.isLandscape && !keyboardPortraitViewportHeight) {
            keyboardPortraitViewportHeight = keyboardCurrentViewportHeight;
        }
    }

    function getViewportHeight() {
        var windowHeight = window.innerHeight;
        //console.log('window.innerHeight is: ' + windowHeight);
        //console.log('kb height is: ' + ionic.keyboard.height);
        //console.log('kb isOpen: ' + ionic.keyboard.isOpen);

        //TODO: add iPad undocked/split kb once kb plugin supports it
        // the keyboard overlays the window on Android fullscreen
        if (!(ionic.Platform.isAndroid() && ionic.Platform.isFullScreen) &&
            (ionic.keyboard.isOpen || ionic.keyboard.isOpening) && !ionic.keyboard.isClosing) {

            return windowHeight + keyboardGetHeight();
        }
        return windowHeight;
    }

    function keyboardHasPlugin() {
        return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
    }

    ionic.Platform.ready(function () {
        keyboardInitViewportHeight();

        window.addEventListener('orientationchange', keyboardOrientationChange);

        // if orientation changes while app is in background, update on resuming
        /*
         if ( ionic.Platform.isWebView() ) {
         document.addEventListener('resume', keyboardInitViewportHeight);

         if (ionic.Platform.isAndroid()) {
         //TODO: onbackpressed to detect keyboard close without focusout or plugin
         }
         }
         */

        // if orientation changes while app is in background, update on resuming
        /*  if ( ionic.Platform.isWebView() ) {
         document.addEventListener('pause', function() {
         window.removeEventListener('orientationchange', keyboardOrientationChange);
         })
         document.addEventListener('resume', function() {
         keyboardInitViewportHeight();
         window.addEventListener('orientationchange', keyboardOrientationChange)
         });
         }*/

        // Android sometimes reports bad innerHeight on window.load
        // try it again in a lil bit to play it safe
        setTimeout(keyboardInitViewportHeight, 999);

        // only initialize the adjustments for the virtual keyboard
        // if a touchstart event happens
        if (window.navigator.msPointerEnabled) {
            document.addEventListener("MSPointerDown", keyboardInit, false);
        } else {
            document.addEventListener('touchstart', keyboardInit, false);
        }
    });


    var viewportTag;
    var viewportProperties = {};

    ionic.viewport = {
        orientation: function () {
            // 0 = Portrait
            // 90 = Landscape
            // not using window.orientation because each device has a different implementation
            return (window.innerWidth > window.innerHeight ? 90 : 0);
        }
    };

    function viewportLoadTag() {
        var x;

        for (x = 0; x < document.head.children.length; x++) {
            if (document.head.children[x].name == 'viewport') {
                viewportTag = document.head.children[x];
                break;
            }
        }

        if (viewportTag) {
            var props = viewportTag.content.toLowerCase().replace(/\s+/g, '').split(',');
            var keyValue;
            for (x = 0; x < props.length; x++) {
                if (props[x]) {
                    keyValue = props[x].split('=');
                    viewportProperties[keyValue[0]] = (keyValue.length > 1 ? keyValue[1] : '_');
                }
            }
            viewportUpdate();
        }
    }

    function viewportUpdate() {
        // unit tests in viewport.unit.js

        var initWidth = viewportProperties.width;
        var initHeight = viewportProperties.height;
        var p = ionic.Platform;
        var version = p.version();
        var DEVICE_WIDTH = 'device-width';
        var DEVICE_HEIGHT = 'device-height';
        var orientation = ionic.viewport.orientation();

        // Most times we're removing the height and adding the width
        // So this is the default to start with, then modify per platform/version/oreintation
        delete viewportProperties.height;
        viewportProperties.width = DEVICE_WIDTH;

        if (p.isIPad()) {
            // iPad

            if (version > 7) {
                // iPad >= 7.1
                // https://issues.apache.org/jira/browse/CB-4323
                delete viewportProperties.width;

            } else {
                // iPad <= 7.0

                if (p.isWebView()) {
                    // iPad <= 7.0 WebView

                    if (orientation == 90) {
                        // iPad <= 7.0 WebView Landscape
                        viewportProperties.height = '0';

                    } else if (version == 7) {
                        // iPad <= 7.0 WebView Portait
                        viewportProperties.height = DEVICE_HEIGHT;
                    }
                } else {
                    // iPad <= 6.1 Browser
                    if (version < 7) {
                        viewportProperties.height = '0';
                    }
                }
            }

        } else if (p.isIOS()) {
            // iPhone

            if (p.isWebView()) {
                // iPhone WebView

                if (version > 7) {
                    // iPhone >= 7.1 WebView
                    delete viewportProperties.width;

                } else if (version < 7) {
                    // iPhone <= 6.1 WebView
                    // if height was set it needs to get removed with this hack for <= 6.1
                    if (initHeight) viewportProperties.height = '0';

                } else if (version == 7) {
                    //iPhone == 7.0 WebView
                    viewportProperties.height = DEVICE_HEIGHT;
                }

            } else {
                // iPhone Browser

                if (version < 7) {
                    // iPhone <= 6.1 Browser
                    // if height was set it needs to get removed with this hack for <= 6.1
                    if (initHeight) viewportProperties.height = '0';
                }
            }

        }

        // only update the viewport tag if there was a change
        if (initWidth !== viewportProperties.width || initHeight !== viewportProperties.height) {
            viewportTagUpdate();
        }
    }

    function viewportTagUpdate() {
        var key, props = [];
        for (key in viewportProperties) {
            if (viewportProperties[key]) {
                props.push(key + (viewportProperties[key] == '_' ? '' : '=' + viewportProperties[key]));
            }
        }

        viewportTag.content = props.join(', ');
    }

    ionic.Platform.ready(function () {
        viewportLoadTag();

        window.addEventListener("orientationchange", function () {
            setTimeout(viewportUpdate, 1000);
        }, false);
    });

    (function (ionic) {
        'use strict';
        ionic.views.View = function () {
            this.initialize.apply(this, arguments);
        };

        ionic.views.View.inherit = ionic.inherit;

        ionic.extend(ionic.views.View.prototype, {
            initialize: function () {
            }
        });

    })(window.ionic);

    /*
     * Scroller
     * http://github.com/zynga/scroller
     *
     * Copyright 2011, Zynga Inc.
     * Licensed under the MIT License.
     * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
     *
     * Based on the work of: Unify Project (unify-project.org)
     * http://unify-project.org
     * Copyright 2011, Deutsche Telekom AG
     * License: MIT + Apache (V2)
     */

    /* jshint eqnull: true */

    /**
     * Generic animation class with support for dropped frames both optional easing and duration.
     *
     * Optional duration is useful when the lifetime is defined by another condition than time
     * e.g. speed of an animating object, etc.
     *
     * Dropped frame logic allows to keep using the same updater logic independent from the actual
     * rendering. This eases a lot of cases where it might be pretty complex to break down a state
     * based on the pure time difference.
     */
    var zyngaCore = {effect: {}};
    (function (global) {
        var time = Date.now || function () {
                return +new Date();
            };
        var desiredFrames = 60;
        var millisecondsPerSecond = 1000;
        var running = {};
        var counter = 1;

        zyngaCore.effect.Animate = {

            /**
             * A requestAnimationFrame wrapper / polyfill.
             *
             * @param callback {Function} The callback to be invoked before the next repaint.
             * @param root {HTMLElement} The root element for the repaint
             */
            requestAnimationFrame: (function () {

                // Check for request animation Frame support
                var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
                var isNative = !!requestFrame;

                if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
                    isNative = false;
                }

                if (isNative) {
                    return function (callback, root) {
                        requestFrame(callback, root);
                    };
                }

                var TARGET_FPS = 60;
                var requests = {};
                var requestCount = 0;
                var rafHandle = 1;
                var intervalHandle = null;
                var lastActive = +new Date();

                return function (callback) {
                    var callbackHandle = rafHandle++;

                    // Store callback
                    requests[callbackHandle] = callback;
                    requestCount++;

                    // Create timeout at first request
                    if (intervalHandle === null) {

                        intervalHandle = setInterval(function () {

                            var time = +new Date();
                            var currentRequests = requests;

                            // Reset data structure before executing callbacks
                            requests = {};
                            requestCount = 0;

                            for (var key in currentRequests) {
                                if (currentRequests.hasOwnProperty(key)) {
                                    currentRequests[key](time);
                                    lastActive = time;
                                }
                            }

                            // Disable the timeout when nothing happens for a certain
                            // period of time
                            if (time - lastActive > 2500) {
                                clearInterval(intervalHandle);
                                intervalHandle = null;
                            }

                        }, 1000 / TARGET_FPS);
                    }

                    return callbackHandle;
                };

            })(),


            /**
             * Stops the given animation.
             *
             * @param id {Integer} Unique animation ID
             * @return {Boolean} Whether the animation was stopped (aka, was running before)
             */
            stop: function (id) {
                var cleared = running[id] != null;
                if (cleared) {
                    running[id] = null;
                }

                return cleared;
            },


            /**
             * Whether the given animation is still running.
             *
             * @param id {Integer} Unique animation ID
             * @return {Boolean} Whether the animation is still running
             */
            isRunning: function (id) {
                return running[id] != null;
            },


            /**
             * Start the animation.
             *
             * @param stepCallback {Function} Pointer to function which is executed on every step.
             *   Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
             * @param verifyCallback {Function} Executed before every animation step.
             *   Signature of the method should be `function() { return continueWithAnimation; }`
             * @param completedCallback {Function}
             *   Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
             * @param duration {Integer} Milliseconds to run the animation
             * @param easingMethod {Function} Pointer to easing function
             *   Signature of the method should be `function(percent) { return modifiedValue; }`
             * @param root {Element} Render root, when available. Used for internal
             *   usage of requestAnimationFrame.
             * @return {Integer} Identifier of animation. Can be used to stop it any time.
             */
            start: function (stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {

                var start = time();
                var lastFrame = start;
                var percent = 0;
                var dropCounter = 0;
                var id = counter++;

                if (!root) {
                    root = document.body;
                }

                // Compacting running db automatically every few new animations
                if (id % 20 === 0) {
                    var newRunning = {};
                    for (var usedId in running) {
                        newRunning[usedId] = true;
                    }
                    running = newRunning;
                }

                // This is the internal step method which is called every few milliseconds
                var step = function (virtual) {

                    // Normalize virtual value
                    var render = virtual !== true;

                    // Get current time
                    var now = time();

                    // Verification is executed before next animation step
                    if (!running[id] || (verifyCallback && !verifyCallback(id))) {

                        running[id] = null;
                        completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
                        return;

                    }

                    // For the current rendering to apply let's update omitted steps in memory.
                    // This is important to bring internal state variables up-to-date with progress in time.
                    if (render) {

                        var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
                        for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
                            step(true);
                            dropCounter++;
                        }

                    }

                    // Compute percent value
                    if (duration) {
                        percent = (now - start) / duration;
                        if (percent > 1) {
                            percent = 1;
                        }
                    }

                    // Execute step callback, then...
                    var value = easingMethod ? easingMethod(percent) : percent;
                    if ((stepCallback(value, now, render) === false || percent === 1) && render) {
                        running[id] = null;
                        completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
                    } else if (render) {
                        lastFrame = now;
                        zyngaCore.effect.Animate.requestAnimationFrame(step, root);
                    }
                };

                // Mark as running
                running[id] = true;

                // Init first step
                zyngaCore.effect.Animate.requestAnimationFrame(step, root);

                // Return unique animation ID
                return id;
            }
        };
    })(window);

    /*
     * Scroller
     * http://github.com/zynga/scroller
     *
     * Copyright 2011, Zynga Inc.
     * Licensed under the MIT License.
     * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
     *
     * Based on the work of: Unify Project (unify-project.org)
     * http://unify-project.org
     * Copyright 2011, Deutsche Telekom AG
     * License: MIT + Apache (V2)
     */

    (function (ionic) {
        var NOOP = function () {
        };

        // Easing Equations (c) 2003 Robert Penner, all rights reserved.
        // Open source under the BSD License.

        /**
         * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
         **/
        var easeOutCubic = function (pos) {
            return (Math.pow((pos - 1), 3) + 1);
        };

        /**
         * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
         **/
        var easeInOutCubic = function (pos) {
            if ((pos /= 0.5) < 1) {
                return 0.5 * Math.pow(pos, 3);
            }

            return 0.5 * (Math.pow((pos - 2), 3) + 2);
        };


        /**
         * ionic.views.Scroll
         * A powerful scroll view with support for bouncing, pull to refresh, and paging.
         * @param   {Object}        options options for the scroll view
         * @class A scroll view system
         * @memberof ionic.views
         */
        ionic.views.Scroll = ionic.views.View.inherit({
            initialize: function (options) {
                var self = this;

                self.__container = options.el;
                self.__content = options.el.firstElementChild;

                //Remove any scrollTop attached to these elements; they are virtual scroll now
                //This also stops on-load-scroll-to-window.location.hash that the browser does
                setTimeout(function () {
                    if (self.__container && self.__content) {
                        self.__container.scrollTop = 0;
                        self.__content.scrollTop = 0;
                    }
                });

                self.options = {

                    /** Disable scrolling on x-axis by default */
                    scrollingX: false,
                    scrollbarX: true,

                    /** Enable scrolling on y-axis */
                    scrollingY: true,
                    scrollbarY: true,

                    startX: 0,
                    startY: 0,

                    /** The amount to dampen mousewheel events */
                    wheelDampen: 6,

                    /** The minimum size the scrollbars scale to while scrolling */
                    minScrollbarSizeX: 5,
                    minScrollbarSizeY: 5,

                    /** Scrollbar fading after scrolling */
                    scrollbarsFade: true,
                    scrollbarFadeDelay: 300,
                    /** The initial fade delay when the pane is resized or initialized */
                    scrollbarResizeFadeDelay: 1000,

                    /** Enable animations for deceleration, snap back, zooming and scrolling */
                    animating: true,

                    /** duration for animations triggered by scrollTo/zoomTo */
                    animationDuration: 250,

                    /** The velocity required to make the scroll view "slide" after touchend */
                    decelVelocityThreshold: 4,

                    /** The velocity required to make the scroll view "slide" after touchend when using paging */
                    decelVelocityThresholdPaging: 4,

                    /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
                    bouncing: true,

                    /** Enable locking to the main axis if user moves only slightly on one of them at start */
                    locking: true,

                    /** Enable pagination mode (switching between full page content panes) */
                    paging: false,

                    /** Enable snapping of content to a configured pixel grid */
                    snapping: false,

                    /** Enable zooming of content via API, fingers and mouse wheel */
                    zooming: false,

                    /** Minimum zoom level */
                    minZoom: 0.5,

                    /** Maximum zoom level */
                    maxZoom: 3,

                    /** Multiply or decrease scrolling speed **/
                    speedMultiplier: 1,

                    deceleration: 0.97,

                    /** Whether to prevent default on a scroll operation to capture drag events **/
                    preventDefault: false,

                    /** Callback that is fired on the later of touch end or deceleration end,
                     provided that another scrolling action has not begun. Used to know
                     when to fade out a scrollbar. */
                    scrollingComplete: NOOP,

                    /** This configures the amount of change applied to deceleration when reaching boundaries  **/
                    penetrationDeceleration: 0.03,

                    /** This configures the amount of change applied to acceleration when reaching boundaries  **/
                    penetrationAcceleration: 0.08,

                    // The ms interval for triggering scroll events
                    scrollEventInterval: 10,

                    freeze: false,

                    getContentWidth: function () {
                        return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
                    },
                    getContentHeight: function () {
                        return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
                    }
                };

                for (var key in options) {
                    self.options[key] = options[key];
                }

                self.hintResize = ionic.debounce(function () {
                    self.resize();
                }, 1000, true);

                self.onScroll = function () {

                    if (!ionic.scroll.isScrolling) {
                        setTimeout(self.setScrollStart, 50);
                    } else {
                        clearTimeout(self.scrollTimer);
                        self.scrollTimer = setTimeout(self.setScrollStop, 80);
                    }

                };

                self.freeze = function (shouldFreeze) {
                    if (arguments.length) {
                        self.options.freeze = shouldFreeze;
                    }
                    return self.options.freeze;
                };

                // We can just use the standard freeze pop in our mouth
                self.freezeShut = self.freeze;

                self.setScrollStart = function () {
                    ionic.scroll.isScrolling = Math.abs(ionic.scroll.lastTop - self.__scrollTop) > 1;
                    clearTimeout(self.scrollTimer);
                    self.scrollTimer = setTimeout(self.setScrollStop, 80);
                };

                self.setScrollStop = function () {
                    ionic.scroll.isScrolling = false;
                    ionic.scroll.lastTop = self.__scrollTop;
                };

                self.triggerScrollEvent = ionic.throttle(function () {
                    self.onScroll();
                    ionic.trigger('scroll', {
                        scrollTop: self.__scrollTop,
                        scrollLeft: self.__scrollLeft,
                        target: self.__container
                    });
                }, self.options.scrollEventInterval);

                self.triggerScrollEndEvent = function () {
                    ionic.trigger('scrollend', {
                        scrollTop: self.__scrollTop,
                        scrollLeft: self.__scrollLeft,
                        target: self.__container
                    });
                };

                self.__scrollLeft = self.options.startX;
                self.__scrollTop = self.options.startY;

                // Get the render update function, initialize event handlers,
                // and calculate the size of the scroll container
                self.__callback = self.getRenderFn();
                self.__initEventHandlers();
                self.__createScrollbars();

            },

            run: function () {
                this.resize();

                // Fade them out
                this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay);
            },


            /*
             ---------------------------------------------------------------------------
             INTERNAL FIELDS :: STATUS
             ---------------------------------------------------------------------------
             */

            /** Whether only a single finger is used in touch handling */
            __isSingleTouch: false,

            /** Whether a touch event sequence is in progress */
            __isTracking: false,

            /** Whether a deceleration animation went to completion. */
            __didDecelerationComplete: false,

            /**
             * Whether a gesture zoom/rotate event is in progress. Activates when
             * a gesturestart event happens. This has higher priority than dragging.
             */
            __isGesturing: false,

            /**
             * Whether the user has moved by such a distance that we have enabled
             * dragging mode. Hint: It's only enabled after some pixels of movement to
             * not interrupt with clicks etc.
             */
            __isDragging: false,

            /**
             * Not touching and dragging anymore, and smoothly animating the
             * touch sequence using deceleration.
             */
            __isDecelerating: false,

            /**
             * Smoothly animating the currently configured change
             */
            __isAnimating: false,


            /*
             ---------------------------------------------------------------------------
             INTERNAL FIELDS :: DIMENSIONS
             ---------------------------------------------------------------------------
             */

            /** Available outer left position (from document perspective) */
            __clientLeft: 0,

            /** Available outer top position (from document perspective) */
            __clientTop: 0,

            /** Available outer width */
            __clientWidth: 0,

            /** Available outer height */
            __clientHeight: 0,

            /** Outer width of content */
            __contentWidth: 0,

            /** Outer height of content */
            __contentHeight: 0,

            /** Snapping width for content */
            __snapWidth: 100,

            /** Snapping height for content */
            __snapHeight: 100,

            /** Height to assign to refresh area */
            __refreshHeight: null,

            /** Whether the refresh process is enabled when the event is released now */
            __refreshActive: false,

            /** Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
            __refreshActivate: null,

            /** Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
            __refreshDeactivate: null,

            /** Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
            __refreshStart: null,

            /** Zoom level */
            __zoomLevel: 1,

            /** Scroll position on x-axis */
            __scrollLeft: 0,

            /** Scroll position on y-axis */
            __scrollTop: 0,

            /** Maximum allowed scroll position on x-axis */
            __maxScrollLeft: 0,

            /** Maximum allowed scroll position on y-axis */
            __maxScrollTop: 0,

            /* Scheduled left position (final position when animating) */
            __scheduledLeft: 0,

            /* Scheduled top position (final position when animating) */
            __scheduledTop: 0,

            /* Scheduled zoom level (final scale when animating) */
            __scheduledZoom: 0,


            /*
             ---------------------------------------------------------------------------
             INTERNAL FIELDS :: LAST POSITIONS
             ---------------------------------------------------------------------------
             */

            /** Left position of finger at start */
            __lastTouchLeft: null,

            /** Top position of finger at start */
            __lastTouchTop: null,

            /** Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
            __lastTouchMove: null,

            /** List of positions, uses three indexes for each state: left, top, timestamp */
            __positions: null,


            /*
             ---------------------------------------------------------------------------
             INTERNAL FIELDS :: DECELERATION SUPPORT
             ---------------------------------------------------------------------------
             */

            /** Minimum left scroll position during deceleration */
            __minDecelerationScrollLeft: null,

            /** Minimum top scroll position during deceleration */
            __minDecelerationScrollTop: null,

            /** Maximum left scroll position during deceleration */
            __maxDecelerationScrollLeft: null,

            /** Maximum top scroll position during deceleration */
            __maxDecelerationScrollTop: null,

            /** Current factor to modify horizontal scroll position with on every step */
            __decelerationVelocityX: null,

            /** Current factor to modify vertical scroll position with on every step */
            __decelerationVelocityY: null,


            /** the browser-specific property to use for transforms */
            __transformProperty: null,
            __perspectiveProperty: null,

            /** scrollbar indicators */
            __indicatorX: null,
            __indicatorY: null,

            /** Timeout for scrollbar fading */
            __scrollbarFadeTimeout: null,

            /** whether we've tried to wait for size already */
            __didWaitForSize: null,
            __sizerTimeout: null,

            __initEventHandlers: function () {
                var self = this;

                // Event Handler
                var container = self.__container;

                // save height when scroll view is shrunk so we don't need to reflow
                var scrollViewOffsetHeight;

                /**
                 * Shrink the scroll view when the keyboard is up if necessary and if the
                 * focused input is below the bottom of the shrunk scroll view, scroll it
                 * into view.
                 */
                self.scrollChildIntoView = function (e) {
                    //console.log("scrollChildIntoView at: " + Date.now());

                    // D
                    var scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
                    // D - A
                    scrollViewOffsetHeight = container.offsetHeight;
                    var alreadyShrunk = self.isShrunkForKeyboard;

                    var isModal = container.parentNode.classList.contains('modal');
                    // 680px is when the media query for 60% modal width kicks in
                    var isInsetModal = isModal && window.innerWidth >= 680;

                    /*
                     *  _______
                     * |---A---| <- top of scroll view
                     * |       |
                     * |---B---| <- keyboard
                     * |   C   | <- input
                     * |---D---| <- initial bottom of scroll view
                     * |___E___| <- bottom of viewport
                     *
                     *  All commented calculations relative to the top of the viewport (ie E
                     *  is the viewport height, not 0)
                     */
                    if (!alreadyShrunk) {
                        // shrink scrollview so we can actually scroll if the input is hidden
                        // if it isn't shrink so we can scroll to inputs under the keyboard
                        // inset modals won't shrink on Android on their own when the keyboard appears
                        if (ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal) {
                            // if there are things below the scroll view account for them and
                            // subtract them from the keyboard height when resizing
                            // E - D                         E                         D
                            var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;

                            // 0 or D - B if D > B           E - B                     E - D
                            var keyboardOffset = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);

                            ionic.requestAnimationFrame(function () {
                                // D - A or B - A if D > B       D - A             max(0, D - B)
                                scrollViewOffsetHeight = scrollViewOffsetHeight - keyboardOffset;
                                container.style.height = scrollViewOffsetHeight + "px";
                                container.style.overflow = "visible";

                                //update scroll view
                                self.resize();
                            });
                        }

                        self.isShrunkForKeyboard = true;
                    }

                    /*
                     *  _______
                     * |---A---| <- top of scroll view
                     * |   *   | <- where we want to scroll to
                     * |--B-D--| <- keyboard, bottom of scroll view
                     * |   C   | <- input
                     * |       |
                     * |___E___| <- bottom of viewport
                     *
                     *  All commented calculations relative to the top of the viewport (ie E
                     *  is the viewport height, not 0)
                     */
                    // if the element is positioned under the keyboard scroll it into view
                    if (e.detail.isElementUnderKeyboard) {

                        ionic.requestAnimationFrame(function () {
                            container.scrollTop = 0;
                            // update D if we shrunk
                            if (self.isShrunkForKeyboard && !alreadyShrunk) {
                                scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
                            }

                            // middle of the scrollview, this is where we want to scroll to
                            // (D - A) / 2
                            var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
                            //console.log("container.offsetHeight: " + scrollViewOffsetHeight);

                            // middle of the input we want to scroll into view
                            // C
                            var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);

                            // distance from middle of input to the bottom of the scroll view
                            // C - D                                C               D
                            var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;

                            //C - D + (D - A)/2          C - D                     (D - A)/ 2
                            var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;

                            if (scrollTop > 0) {
                                if (ionic.Platform.isIOS()) ionic.tap.cloneFocusedInput(container, self);
                                self.scrollBy(0, scrollTop, true);
                                self.onScroll();
                            }
                        });
                    }

                    // Only the first scrollView parent of the element that broadcasted this event
                    // (the active element that needs to be shown) should receive this event
                    e.stopPropagation();
                };

                self.resetScrollView = function () {
                    //return scrollview to original height once keyboard has hidden
                    if (self.isShrunkForKeyboard) {
                        self.isShrunkForKeyboard = false;
                        container.style.height = "";
                        container.style.overflow = "";
                    }
                    self.resize();
                };

                //Broadcasted when keyboard is shown on some platforms.
                //See js/utils/keyboard.js
                container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);

                // Listen on document because container may not have had the last
                // keyboardActiveElement, for example after closing a modal with a focused
                // input and returning to a previously resized scroll view in an ion-content.
                // Since we can only resize scroll views that are currently visible, just resize
                // the current scroll view when the keyboard is closed.
                document.addEventListener('resetScrollView', self.resetScrollView);

                function getEventTouches(e) {
                    return e.touches && e.touches.length ? e.touches : [{
                            pageX: e.pageX,
                            pageY: e.pageY
                        }];
                }

                self.touchStart = function (e) {
                    self.startCoordinates = ionic.tap.pointerCoord(e);

                    if (ionic.tap.ignoreScrollStart(e)) {
                        return;
                    }

                    self.__isDown = true;

                    if (ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT') {
                        // do not start if the target is a text input
                        // if there is a touchmove on this input, then we can start the scroll
                        self.__hasStarted = false;
                        return;
                    }

                    self.__isSelectable = true;
                    self.__enableScrollY = true;
                    self.__hasStarted = true;
                    self.doTouchStart(getEventTouches(e), e.timeStamp);
                    e.preventDefault();
                };

                self.touchMove = function (e) {
                    if (self.options.freeze || !self.__isDown ||
                        (!self.__isDown && e.defaultPrevented) ||
                        (e.target.tagName === 'TEXTAREA' && e.target.parentElement.querySelector(':focus'))) {
                        return;
                    }

                    if (!self.__hasStarted && ( ionic.tap.containsOrIsTextInput(e.target) || e.target.tagName === 'SELECT' )) {
                        // the target is a text input and scroll has started
                        // since the text input doesn't start on touchStart, do it here
                        self.__hasStarted = true;
                        self.doTouchStart(getEventTouches(e), e.timeStamp);
                        e.preventDefault();
                        return;
                    }

                    if (self.startCoordinates) {
                        // we have start coordinates, so get this touch move's current coordinates
                        var currentCoordinates = ionic.tap.pointerCoord(e);

                        if (self.__isSelectable &&
                            ionic.tap.isTextInput(e.target) &&
                            Math.abs(self.startCoordinates.x - currentCoordinates.x) > 20) {
                            // user slid the text input's caret on its x axis, disable any future y scrolling
                            self.__enableScrollY = false;
                            self.__isSelectable = true;
                        }

                        if (self.__enableScrollY && Math.abs(self.startCoordinates.y - currentCoordinates.y) > 10) {
                            // user scrolled the entire view on the y axis
                            // disabled being able to select text on an input
                            // hide the input which has focus, and show a cloned one that doesn't have focus
                            self.__isSelectable = false;
                            ionic.tap.cloneFocusedInput(container, self);
                        }
                    }

                    self.doTouchMove(getEventTouches(e), e.timeStamp, e.scale);
                    self.__isDown = true;
                };

                self.touchMoveBubble = function (e) {
                    if (self.__isDown && self.options.preventDefault) {
                        e.preventDefault();
                    }
                };

                self.touchEnd = function (e) {
                    if (!self.__isDown) return;

                    self.doTouchEnd(e, e.timeStamp);
                    self.__isDown = false;
                    self.__hasStarted = false;
                    self.__isSelectable = true;
                    self.__enableScrollY = true;

                    if (!self.__isDragging && !self.__isDecelerating && !self.__isAnimating) {
                        ionic.tap.removeClonedInputs(container, self);
                    }
                };

                self.mouseWheel = ionic.animationFrameThrottle(function (e) {
                    var scrollParent = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'ionic-scroll');
                    if (!self.options.freeze && scrollParent === self.__container) {

                        self.hintResize();
                        self.scrollBy(
                            (e.wheelDeltaX || e.deltaX || 0) / self.options.wheelDampen,
                            (-e.wheelDeltaY || e.deltaY || 0) / self.options.wheelDampen
                        );

                        self.__fadeScrollbars('in');
                        clearTimeout(self.__wheelHideBarTimeout);
                        self.__wheelHideBarTimeout = setTimeout(function () {
                            self.__fadeScrollbars('out');
                        }, 100);
                    }
                });

                if ('ontouchstart' in window) {
                    // Touch Events
                    container.addEventListener("touchstart", self.touchStart, false);
                    if (self.options.preventDefault) container.addEventListener("touchmove", self.touchMoveBubble, false);
                    document.addEventListener("touchmove", self.touchMove, false);
                    document.addEventListener("touchend", self.touchEnd, false);
                    document.addEventListener("touchcancel", self.touchEnd, false);
                    document.addEventListener("wheel", self.mouseWheel, false);

                } else if (window.navigator.pointerEnabled) {
                    // Pointer Events
                    container.addEventListener("pointerdown", self.touchStart, false);
                    if (self.options.preventDefault) container.addEventListener("pointermove", self.touchMoveBubble, false);
                    document.addEventListener("pointermove", self.touchMove, false);
                    document.addEventListener("pointerup", self.touchEnd, false);
                    document.addEventListener("pointercancel", self.touchEnd, false);
                    document.addEventListener("wheel", self.mouseWheel, false);

                } else if (window.navigator.msPointerEnabled) {
                    // IE10, WP8 (Pointer Events)
                    container.addEventListener("MSPointerDown", self.touchStart, false);
                    if (self.options.preventDefault) container.addEventListener("MSPointerMove", self.touchMoveBubble, false);
                    document.addEventListener("MSPointerMove", self.touchMove, false);
                    document.addEventListener("MSPointerUp", self.touchEnd, false);
                    document.addEventListener("MSPointerCancel", self.touchEnd, false);
                    document.addEventListener("wheel", self.mouseWheel, false);

                } else {
                    // Mouse Events
                    var mousedown = false;

                    self.mouseDown = function (e) {
                        if (ionic.tap.ignoreScrollStart(e) || e.target.tagName === 'SELECT') {
                            return;
                        }
                        self.doTouchStart(getEventTouches(e), e.timeStamp);

                        if (!ionic.tap.isTextInput(e.target)) {
                            e.preventDefault();
                        }
                        mousedown = true;
                    };

                    self.mouseMove = function (e) {
                        if (self.options.freeze || !mousedown || (!mousedown && e.defaultPrevented)) {
                            return;
                        }

                        self.doTouchMove(getEventTouches(e), e.timeStamp);

                        mousedown = true;
                    };

                    self.mouseMoveBubble = function (e) {
                        if (mousedown && self.options.preventDefault) {
                            e.preventDefault();
                        }
                    };

                    self.mouseUp = function (e) {
                        if (!mousedown) {
                            return;
                        }

                        self.doTouchEnd(e, e.timeStamp);

                        mousedown = false;
                    };

                    container.addEventListener("mousedown", self.mouseDown, false);
                    if (self.options.preventDefault) container.addEventListener("mousemove", self.mouseMoveBubble, false);
                    document.addEventListener("mousemove", self.mouseMove, false);
                    document.addEventListener("mouseup", self.mouseUp, false);
                    document.addEventListener('mousewheel', self.mouseWheel, false);
                    document.addEventListener('wheel', self.mouseWheel, false);
                }
            },

            __cleanup: function () {
                var self = this;
                var container = self.__container;

                container.removeEventListener('touchstart', self.touchStart);
                container.removeEventListener('touchmove', self.touchMoveBubble);
                document.removeEventListener('touchmove', self.touchMove);
                document.removeEventListener('touchend', self.touchEnd);
                document.removeEventListener('touchcancel', self.touchEnd);

                container.removeEventListener("pointerdown", self.touchStart);
                container.removeEventListener("pointermove", self.touchMoveBubble);
                document.removeEventListener("pointermove", self.touchMove);
                document.removeEventListener("pointerup", self.touchEnd);
                document.removeEventListener("pointercancel", self.touchEnd);

                container.removeEventListener("MSPointerDown", self.touchStart);
                container.removeEventListener("MSPointerMove", self.touchMoveBubble);
                document.removeEventListener("MSPointerMove", self.touchMove);
                document.removeEventListener("MSPointerUp", self.touchEnd);
                document.removeEventListener("MSPointerCancel", self.touchEnd);

                container.removeEventListener("mousedown", self.mouseDown);
                container.removeEventListener("mousemove", self.mouseMoveBubble);
                document.removeEventListener("mousemove", self.mouseMove);
                document.removeEventListener("mouseup", self.mouseUp);
                document.removeEventListener('mousewheel', self.mouseWheel);
                document.removeEventListener('wheel', self.mouseWheel);

                container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
                document.removeEventListener('resetScrollView', self.resetScrollView);

                ionic.tap.removeClonedInputs(container, self);

                delete self.__container;
                delete self.__content;
                delete self.__indicatorX;
                delete self.__indicatorY;
                delete self.options.el;

                self.__callback = self.scrollChildIntoView = self.resetScrollView = NOOP;

                self.mouseMove = self.mouseDown = self.mouseUp = self.mouseWheel =
                    self.touchStart = self.touchMove = self.touchEnd = self.touchCancel = NOOP;

                self.resize = self.scrollTo = self.zoomTo =
                    self.__scrollingComplete = NOOP;
                container = null;
            },

            /** Create a scroll bar div with the given direction **/
            __createScrollbar: function (direction) {
                var bar = document.createElement('div'),
                    indicator = document.createElement('div');

                indicator.className = 'scroll-bar-indicator scroll-bar-fade-out';

                if (direction == 'h') {
                    bar.className = 'scroll-bar scroll-bar-h';
                } else {
                    bar.className = 'scroll-bar scroll-bar-v';
                }

                bar.appendChild(indicator);
                return bar;
            },

            __createScrollbars: function () {
                var self = this;
                var indicatorX, indicatorY;

                if (self.options.scrollingX) {
                    indicatorX = {
                        el: self.__createScrollbar('h'),
                        sizeRatio: 1
                    };
                    indicatorX.indicator = indicatorX.el.children[0];

                    if (self.options.scrollbarX) {
                        self.__container.appendChild(indicatorX.el);
                    }
                    self.__indicatorX = indicatorX;
                }

                if (self.options.scrollingY) {
                    indicatorY = {
                        el: self.__createScrollbar('v'),
                        sizeRatio: 1
                    };
                    indicatorY.indicator = indicatorY.el.children[0];

                    if (self.options.scrollbarY) {
                        self.__container.appendChild(indicatorY.el);
                    }
                    self.__indicatorY = indicatorY;
                }
            },

            __resizeScrollbars: function () {
                var self = this;

                // Update horiz bar
                if (self.__indicatorX) {
                    var width = Math.max(Math.round(self.__clientWidth * self.__clientWidth / (self.__contentWidth)), 20);
                    if (width > self.__contentWidth) {
                        width = 0;
                    }
                    if (width !== self.__indicatorX.size) {
                        ionic.requestAnimationFrame(function () {
                            self.__indicatorX.indicator.style.width = width + 'px';
                        });
                    }
                    self.__indicatorX.size = width;
                    self.__indicatorX.minScale = self.options.minScrollbarSizeX / width;
                    self.__indicatorX.maxPos = self.__clientWidth - width;
                    self.__indicatorX.sizeRatio = self.__maxScrollLeft ? self.__indicatorX.maxPos / self.__maxScrollLeft : 1;
                }

                // Update vert bar
                if (self.__indicatorY) {
                    var height = Math.max(Math.round(self.__clientHeight * self.__clientHeight / (self.__contentHeight)), 20);
                    if (height > self.__contentHeight) {
                        height = 0;
                    }
                    if (height !== self.__indicatorY.size) {
                        ionic.requestAnimationFrame(function () {
                            self.__indicatorY && (self.__indicatorY.indicator.style.height = height + 'px');
                        });
                    }
                    self.__indicatorY.size = height;
                    self.__indicatorY.minScale = self.options.minScrollbarSizeY / height;
                    self.__indicatorY.maxPos = self.__clientHeight - height;
                    self.__indicatorY.sizeRatio = self.__maxScrollTop ? self.__indicatorY.maxPos / self.__maxScrollTop : 1;
                }
            },

            /**
             * Move and scale the scrollbars as the page scrolls.
             */
            __repositionScrollbars: function () {
                var self = this,
                    heightScale, widthScale,
                    widthDiff, heightDiff,
                    x, y,
                    xstop = 0, ystop = 0;

                if (self.__indicatorX) {
                    // Handle the X scrollbar

                    // Don't go all the way to the right if we have a vertical scrollbar as well
                    if (self.__indicatorY) xstop = 10;

                    x = Math.round(self.__indicatorX.sizeRatio * self.__scrollLeft) || 0;

                    // The the difference between the last content X position, and our overscrolled one
                    widthDiff = self.__scrollLeft - (self.__maxScrollLeft - xstop);

                    if (self.__scrollLeft < 0) {

                        widthScale = Math.max(self.__indicatorX.minScale,
                            (self.__indicatorX.size - Math.abs(self.__scrollLeft)) / self.__indicatorX.size);

                        // Stay at left
                        x = 0;

                        // Make sure scale is transformed from the left/center origin point
                        self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'left center';
                    } else if (widthDiff > 0) {

                        widthScale = Math.max(self.__indicatorX.minScale,
                            (self.__indicatorX.size - widthDiff) / self.__indicatorX.size);

                        // Stay at the furthest x for the scrollable viewport
                        x = self.__indicatorX.maxPos - xstop;

                        // Make sure scale is transformed from the right/center origin point
                        self.__indicatorX.indicator.style[self.__transformOriginProperty] = 'right center';

                    } else {

                        // Normal motion
                        x = Math.min(self.__maxScrollLeft, Math.max(0, x));
                        widthScale = 1;

                    }

                    var translate3dX = 'translate3d(' + x + 'px, 0, 0) scaleX(' + widthScale + ')';
                    if (self.__indicatorX.transformProp !== translate3dX) {
                        self.__indicatorX.indicator.style[self.__transformProperty] = translate3dX;
                        self.__indicatorX.transformProp = translate3dX;
                    }
                }

                if (self.__indicatorY) {

                    y = Math.round(self.__indicatorY.sizeRatio * self.__scrollTop) || 0;

                    // Don't go all the way to the right if we have a vertical scrollbar as well
                    if (self.__indicatorX) ystop = 10;

                    heightDiff = self.__scrollTop - (self.__maxScrollTop - ystop);

                    if (self.__scrollTop < 0) {

                        heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - Math.abs(self.__scrollTop)) / self.__indicatorY.size);

                        // Stay at top
                        y = 0;

                        // Make sure scale is transformed from the center/top origin point
                        if (self.__indicatorY.originProp !== 'center top') {
                            self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center top';
                            self.__indicatorY.originProp = 'center top';
                        }

                    } else if (heightDiff > 0) {

                        heightScale = Math.max(self.__indicatorY.minScale, (self.__indicatorY.size - heightDiff) / self.__indicatorY.size);

                        // Stay at bottom of scrollable viewport
                        y = self.__indicatorY.maxPos - ystop;

                        // Make sure scale is transformed from the center/bottom origin point
                        if (self.__indicatorY.originProp !== 'center bottom') {
                            self.__indicatorY.indicator.style[self.__transformOriginProperty] = 'center bottom';
                            self.__indicatorY.originProp = 'center bottom';
                        }

                    } else {

                        // Normal motion
                        y = Math.min(self.__maxScrollTop, Math.max(0, y));
                        heightScale = 1;

                    }

                    var translate3dY = 'translate3d(0,' + y + 'px, 0) scaleY(' + heightScale + ')';
                    if (self.__indicatorY.transformProp !== translate3dY) {
                        self.__indicatorY.indicator.style[self.__transformProperty] = translate3dY;
                        self.__indicatorY.transformProp = translate3dY;
                    }
                }
            },

            __fadeScrollbars: function (direction, delay) {
                var self = this;

                if (!self.options.scrollbarsFade) {
                    return;
                }

                var className = 'scroll-bar-fade-out';

                if (self.options.scrollbarsFade === true) {
                    clearTimeout(self.__scrollbarFadeTimeout);

                    if (direction == 'in') {
                        if (self.__indicatorX) {
                            self.__indicatorX.indicator.classList.remove(className);
                        }
                        if (self.__indicatorY) {
                            self.__indicatorY.indicator.classList.remove(className);
                        }
                    } else {
                        self.__scrollbarFadeTimeout = setTimeout(function () {
                            if (self.__indicatorX) {
                                self.__indicatorX.indicator.classList.add(className);
                            }
                            if (self.__indicatorY) {
                                self.__indicatorY.indicator.classList.add(className);
                            }
                        }, delay || self.options.scrollbarFadeDelay);
                    }
                }
            },

            __scrollingComplete: function () {
                this.options.scrollingComplete();
                ionic.tap.removeClonedInputs(this.__container, this);
                this.__fadeScrollbars('out');
            },

            resize: function (continueScrolling) {
                var self = this;
                if (!self.__container || !self.options) return;

                // Update Scroller dimensions for changed content
                // Add padding to bottom of content
                self.setDimensions(
                    self.__container.clientWidth,
                    self.__container.clientHeight,
                    self.options.getContentWidth(),
                    self.options.getContentHeight(),
                    continueScrolling
                );
            },
            /*
             ---------------------------------------------------------------------------
             PUBLIC API
             ---------------------------------------------------------------------------
             */

            getRenderFn: function () {
                var self = this;

                var content = self.__content;

                var docStyle = document.documentElement.style;

                var engine;
                if ('MozAppearance' in docStyle) {
                    engine = 'gecko';
                } else if ('WebkitAppearance' in docStyle) {
                    engine = 'webkit';
                } else if (typeof navigator.cpuClass === 'string') {
                    engine = 'trident';
                }

                var vendorPrefix = {
                    trident: 'ms',
                    gecko: 'Moz',
                    webkit: 'Webkit',
                    presto: 'O'
                }[engine];

                var helperElem = document.createElement("div");
                var undef;

                var perspectiveProperty = vendorPrefix + "Perspective";
                var transformProperty = vendorPrefix + "Transform";
                var transformOriginProperty = vendorPrefix + 'TransformOrigin';

                self.__perspectiveProperty = transformProperty;
                self.__transformProperty = transformProperty;
                self.__transformOriginProperty = transformOriginProperty;

                if (helperElem.style[perspectiveProperty] !== undef) {

                    return function (left, top, zoom, wasResize) {
                        var translate3d = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')';
                        if (translate3d !== self.contentTransform) {
                            content.style[transformProperty] = translate3d;
                            self.contentTransform = translate3d;
                        }
                        self.__repositionScrollbars();
                        if (!wasResize) {
                            self.triggerScrollEvent();
                        }
                    };

                } else if (helperElem.style[transformProperty] !== undef) {

                    return function (left, top, zoom, wasResize) {
                        content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')';
                        self.__repositionScrollbars();
                        if (!wasResize) {
                            self.triggerScrollEvent();
                        }
                    };

                } else {

                    return function (left, top, zoom, wasResize) {
                        content.style.marginLeft = left ? (-left / zoom) + 'px' : '';
                        content.style.marginTop = top ? (-top / zoom) + 'px' : '';
                        content.style.zoom = zoom || '';
                        self.__repositionScrollbars();
                        if (!wasResize) {
                            self.triggerScrollEvent();
                        }
                    };

                }
            },


            /**
             * Configures the dimensions of the client (outer) and content (inner) elements.
             * Requires the available space for the outer element and the outer size of the inner element.
             * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
             *
             * @param clientWidth {Integer} Inner width of outer element
             * @param clientHeight {Integer} Inner height of outer element
             * @param contentWidth {Integer} Outer width of inner element
             * @param contentHeight {Integer} Outer height of inner element
             */
            setDimensions: function (clientWidth, clientHeight, contentWidth, contentHeight, continueScrolling) {
                var self = this;

                if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
                    // this scrollview isn't rendered, don't bother
                    return;
                }

                // Only update values which are defined
                if (clientWidth === +clientWidth) {
                    self.__clientWidth = clientWidth;
                }

                if (clientHeight === +clientHeight) {
                    self.__clientHeight = clientHeight;
                }

                if (contentWidth === +contentWidth) {
                    self.__contentWidth = contentWidth;
                }

                if (contentHeight === +contentHeight) {
                    self.__contentHeight = contentHeight;
                }

                // Refresh maximums
                self.__computeScrollMax();
                self.__resizeScrollbars();

                // Refresh scroll position
                if (!continueScrolling) {
                    self.scrollTo(self.__scrollLeft, self.__scrollTop, true, null, true);
                }

            },


            /**
             * Sets the client coordinates in relation to the document.
             *
             * @param left {Integer} Left position of outer element
             * @param top {Integer} Top position of outer element
             */
            setPosition: function (left, top) {
                this.__clientLeft = left || 0;
                this.__clientTop = top || 0;
            },


            /**
             * Configures the snapping (when snapping is active)
             *
             * @param width {Integer} Snapping width
             * @param height {Integer} Snapping height
             */
            setSnapSize: function (width, height) {
                this.__snapWidth = width;
                this.__snapHeight = height;
            },


            /**
             * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
             * the user event is released during visibility of this zone. This was introduced by some apps on iOS like
             * the official Twitter client.
             *
             * @param height {Integer} Height of pull-to-refresh zone on top of rendered list
             * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
             * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
             * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
             * @param showCallback {Function} Callback to execute when the refresher should be shown. This is for showing the refresher during a negative scrollTop.
             * @param hideCallback {Function} Callback to execute when the refresher should be hidden. This is for hiding the refresher when it's behind the nav bar.
             * @param tailCallback {Function} Callback to execute just before the refresher returns to it's original state. This is for zooming out the refresher.
             * @param pullProgressCallback Callback to state the progress while pulling to refresh
             */
            activatePullToRefresh: function (height, refresherMethods) {
                var self = this;

                self.__refreshHeight = height;
                self.__refreshActivate = function () {
                    ionic.requestAnimationFrame(refresherMethods.activate);
                };
                self.__refreshDeactivate = function () {
                    ionic.requestAnimationFrame(refresherMethods.deactivate);
                };
                self.__refreshStart = function () {
                    ionic.requestAnimationFrame(refresherMethods.start);
                };
                self.__refreshShow = function () {
                    ionic.requestAnimationFrame(refresherMethods.show);
                };
                self.__refreshHide = function () {
                    ionic.requestAnimationFrame(refresherMethods.hide);
                };
                self.__refreshTail = function () {
                    ionic.requestAnimationFrame(refresherMethods.tail);
                };
                self.__refreshTailTime = 100;
                self.__minSpinTime = 600;
            },


            /**
             * Starts pull-to-refresh manually.
             */
            triggerPullToRefresh: function () {
                // Use publish instead of scrollTo to allow scrolling to out of boundary position
                // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
                this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);

                var d = new Date();
                this.refreshStartTime = d.getTime();

                if (this.__refreshStart) {
                    this.__refreshStart();
                }
            },


            /**
             * Signalizes that pull-to-refresh is finished.
             */
            finishPullToRefresh: function () {
                var self = this;
                // delay to make sure the spinner has a chance to spin for a split second before it's dismissed
                var d = new Date();
                var delay = 0;
                if (self.refreshStartTime + self.__minSpinTime > d.getTime()) {
                    delay = self.refreshStartTime + self.__minSpinTime - d.getTime();
                }
                setTimeout(function () {
                    if (self.__refreshTail) {
                        self.__refreshTail();
                    }
                    setTimeout(function () {
                        self.__refreshActive = false;
                        if (self.__refreshDeactivate) {
                            self.__refreshDeactivate();
                        }
                        if (self.__refreshHide) {
                            self.__refreshHide();
                        }

                        self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
                    }, self.__refreshTailTime);
                }, delay);
            },


            /**
             * Returns the scroll position and zooming values
             *
             * @return {Map} `left` and `top` scroll position and `zoom` level
             */
            getValues: function () {
                return {
                    left: this.__scrollLeft,
                    top: this.__scrollTop,
                    zoom: this.__zoomLevel
                };
            },


            /**
             * Returns the maximum scroll values
             *
             * @return {Map} `left` and `top` maximum scroll values
             */
            getScrollMax: function () {
                return {
                    left: this.__maxScrollLeft,
                    top: this.__maxScrollTop
                };
            },


            /**
             * Zooms to the given level. Supports optional animation. Zooms
             * the center when no coordinates are given.
             *
             * @param level {Number} Level to zoom to
             * @param animate {Boolean} Whether to use animation
             * @param originLeft {Number} Zoom in at given left coordinate
             * @param originTop {Number} Zoom in at given top coordinate
             */
            zoomTo: function (level, animate, originLeft, originTop) {
                var self = this;

                if (!self.options.zooming) {
                    throw new Error("Zooming is not enabled!");
                }

                // Stop deceleration
                if (self.__isDecelerating) {
                    zyngaCore.effect.Animate.stop(self.__isDecelerating);
                    self.__isDecelerating = false;
                }

                var oldLevel = self.__zoomLevel;

                // Normalize input origin to center of viewport if not defined
                if (originLeft == null) {
                    originLeft = self.__clientWidth / 2;
                }

                if (originTop == null) {
                    originTop = self.__clientHeight / 2;
                }

                // Limit level according to configuration
                level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);

                // Recompute maximum values while temporary tweaking maximum scroll ranges
                self.__computeScrollMax(level);

                // Recompute left and top coordinates based on new zoom level
                var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
                var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;

                // Limit x-axis
                if (left > self.__maxScrollLeft) {
                    left = self.__maxScrollLeft;
                } else if (left < 0) {
                    left = 0;
                }

                // Limit y-axis
                if (top > self.__maxScrollTop) {
                    top = self.__maxScrollTop;
                } else if (top < 0) {
                    top = 0;
                }

                // Push values out
                self.__publish(left, top, level, animate);

            },


            /**
             * Zooms the content by the given factor.
             *
             * @param factor {Number} Zoom by given factor
             * @param animate {Boolean} Whether to use animation
             * @param originLeft {Number} Zoom in at given left coordinate
             * @param originTop {Number} Zoom in at given top coordinate
             */
            zoomBy: function (factor, animate, originLeft, originTop) {
                this.zoomTo(this.__zoomLevel * factor, animate, originLeft, originTop);
            },


            /**
             * Scrolls to the given position. Respect limitations and snapping automatically.
             *
             * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
             * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
             * @param animate {Boolean} Whether the scrolling should happen using an animation
             * @param zoom {Number} Zoom level to go to
             */
            scrollTo: function (left, top, animate, zoom, wasResize) {
                var self = this;

                // Stop deceleration
                if (self.__isDecelerating) {
                    zyngaCore.effect.Animate.stop(self.__isDecelerating);
                    self.__isDecelerating = false;
                }

                // Correct coordinates based on new zoom level
                if (zoom != null && zoom !== self.__zoomLevel) {

                    if (!self.options.zooming) {
                        throw new Error("Zooming is not enabled!");
                    }

                    left *= zoom;
                    top *= zoom;

                    // Recompute maximum values while temporary tweaking maximum scroll ranges
                    self.__computeScrollMax(zoom);

                } else {

                    // Keep zoom when not defined
                    zoom = self.__zoomLevel;

                }

                if (!self.options.scrollingX) {

                    left = self.__scrollLeft;

                } else {

                    if (self.options.paging) {
                        left = Math.round(left / self.__clientWidth) * self.__clientWidth;
                    } else if (self.options.snapping) {
                        left = Math.round(left / self.__snapWidth) * self.__snapWidth;
                    }

                }

                if (!self.options.scrollingY) {

                    top = self.__scrollTop;

                } else {

                    if (self.options.paging) {
                        top = Math.round(top / self.__clientHeight) * self.__clientHeight;
                    } else if (self.options.snapping) {
                        top = Math.round(top / self.__snapHeight) * self.__snapHeight;
                    }

                }

                // Limit for allowed ranges
                left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
                top = Math.max(Math.min(self.__maxScrollTop, top), 0);

                // Don't animate when no change detected, still call publish to make sure
                // that rendered position is really in-sync with internal data
                if (left === self.__scrollLeft && top === self.__scrollTop) {
                    animate = false;
                }

                // Publish new values
                self.__publish(left, top, zoom, animate, wasResize);

            },


            /**
             * Scroll by the given offset
             *
             * @param left {Number} Scroll x-axis by given offset
             * @param top {Number} Scroll y-axis by given offset
             * @param animate {Boolean} Whether to animate the given change
             */
            scrollBy: function (left, top, animate) {
                var self = this;

                var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
                var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;

                self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
            },


            /*
             ---------------------------------------------------------------------------
             EVENT CALLBACKS
             ---------------------------------------------------------------------------
             */

            /**
             * Mouse wheel handler for zooming support
             */
            doMouseZoom: function (wheelDelta, timeStamp, pageX, pageY) {
                var change = wheelDelta > 0 ? 0.97 : 1.03;
                return this.zoomTo(this.__zoomLevel * change, false, pageX - this.__clientLeft, pageY - this.__clientTop);
            },

            /**
             * Touch start handler for scrolling support
             */
            doTouchStart: function (touches, timeStamp) {
                var self = this;

                // remember if the deceleration was just stopped
                self.__decStopped = !!(self.__isDecelerating || self.__isAnimating);

                self.hintResize();

                if (timeStamp instanceof Date) {
                    timeStamp = timeStamp.valueOf();
                }
                if (typeof timeStamp !== "number") {
                    timeStamp = Date.now();
                }

                // Reset interruptedAnimation flag
                self.__interruptedAnimation = true;

                // Stop deceleration
                if (self.__isDecelerating) {
                    zyngaCore.effect.Animate.stop(self.__isDecelerating);
                    self.__isDecelerating = false;
                    self.__interruptedAnimation = true;
                }

                // Stop animation
                if (self.__isAnimating) {
                    zyngaCore.effect.Animate.stop(self.__isAnimating);
                    self.__isAnimating = false;
                    self.__interruptedAnimation = true;
                }

                // Use center point when dealing with two fingers
                var currentTouchLeft, currentTouchTop;
                var isSingleTouch = touches.length === 1;
                if (isSingleTouch) {
                    currentTouchLeft = touches[0].pageX;
                    currentTouchTop = touches[0].pageY;
                } else {
                    currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
                    currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
                }

                // Store initial positions
                self.__initialTouchLeft = currentTouchLeft;
                self.__initialTouchTop = currentTouchTop;

                // Store initial touchList for scale calculation
                self.__initialTouches = touches;

                // Store current zoom level
                self.__zoomLevelStart = self.__zoomLevel;

                // Store initial touch positions
                self.__lastTouchLeft = currentTouchLeft;
                self.__lastTouchTop = currentTouchTop;

                // Store initial move time stamp
                self.__lastTouchMove = timeStamp;

                // Reset initial scale
                self.__lastScale = 1;

                // Reset locking flags
                self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
                self.__enableScrollY = !isSingleTouch && self.options.scrollingY;

                // Reset tracking flag
                self.__isTracking = true;

                // Reset deceleration complete flag
                self.__didDecelerationComplete = false;

                // Dragging starts directly with two fingers, otherwise lazy with an offset
                self.__isDragging = !isSingleTouch;

                // Some features are disabled in multi touch scenarios
                self.__isSingleTouch = isSingleTouch;

                // Clearing data structure
                self.__positions = [];

            },


            /**
             * Touch move handler for scrolling support
             */
            doTouchMove: function (touches, timeStamp, scale) {
                if (timeStamp instanceof Date) {
                    timeStamp = timeStamp.valueOf();
                }
                if (typeof timeStamp !== "number") {
                    timeStamp = Date.now();
                }

                var self = this;

                // Ignore event when tracking is not enabled (event might be outside of element)
                if (!self.__isTracking) {
                    return;
                }

                var currentTouchLeft, currentTouchTop;

                // Compute move based around of center of fingers
                if (touches.length === 2) {
                    currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
                    currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;

                    // Calculate scale when not present and only when touches are used
                    if (!scale && self.options.zooming) {
                        scale = self.__getScale(self.__initialTouches, touches);
                    }
                } else {
                    currentTouchLeft = touches[0].pageX;
                    currentTouchTop = touches[0].pageY;
                }

                var positions = self.__positions;

                // Are we already is dragging mode?
                if (self.__isDragging) {
                    self.__decStopped = false;

                    // Compute move distance
                    var moveX = currentTouchLeft - self.__lastTouchLeft;
                    var moveY = currentTouchTop - self.__lastTouchTop;

                    // Read previous scroll position and zooming
                    var scrollLeft = self.__scrollLeft;
                    var scrollTop = self.__scrollTop;
                    var level = self.__zoomLevel;

                    // Work with scaling
                    if (scale != null && self.options.zooming) {

                        var oldLevel = level;

                        // Recompute level based on previous scale and new scale
                        level = level / self.__lastScale * scale;

                        // Limit level according to configuration
                        level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);

                        // Only do further compution when change happened
                        if (oldLevel !== level) {

                            // Compute relative event position to container
                            var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
                            var currentTouchTopRel = currentTouchTop - self.__clientTop;

                            // Recompute left and top coordinates based on new zoom level
                            scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel;
                            scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel;

                            // Recompute max scroll values
                            self.__computeScrollMax(level);

                        }
                    }

                    if (self.__enableScrollX) {

                        scrollLeft -= moveX * self.options.speedMultiplier;
                        var maxScrollLeft = self.__maxScrollLeft;

                        if (scrollLeft > maxScrollLeft || scrollLeft < 0) {

                            // Slow down on the edges
                            if (self.options.bouncing) {

                                scrollLeft += (moveX / 2 * self.options.speedMultiplier);

                            } else if (scrollLeft > maxScrollLeft) {

                                scrollLeft = maxScrollLeft;

                            } else {

                                scrollLeft = 0;

                            }
                        }
                    }

                    // Compute new vertical scroll position
                    if (self.__enableScrollY) {

                        scrollTop -= moveY * self.options.speedMultiplier;
                        var maxScrollTop = self.__maxScrollTop;

                        if (scrollTop > maxScrollTop || scrollTop < 0) {

                            // Slow down on the edges
                            if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) {

                                scrollTop += (moveY / 2 * self.options.speedMultiplier);

                                // Support pull-to-refresh (only when only y is scrollable)
                                if (!self.__enableScrollX && self.__refreshHeight != null) {

                                    // hide the refresher when it's behind the header bar in case of header transparency
                                    if (scrollTop < 0) {
                                        self.__refreshHidden = false;
                                        self.__refreshShow();
                                    } else {
                                        self.__refreshHide();
                                        self.__refreshHidden = true;
                                    }

                                    if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {

                                        self.__refreshActive = true;
                                        if (self.__refreshActivate) {
                                            self.__refreshActivate();
                                        }

                                    } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {

                                        self.__refreshActive = false;
                                        if (self.__refreshDeactivate) {
                                            self.__refreshDeactivate();
                                        }

                                    }
                                }

                            } else if (scrollTop > maxScrollTop) {

                                scrollTop = maxScrollTop;

                            } else {

                                scrollTop = 0;

                            }
                        } else if (self.__refreshHeight && !self.__refreshHidden) {
                            // if a positive scroll value and the refresher is still not hidden, hide it
                            self.__refreshHide();
                            self.__refreshHidden = true;
                        }
                    }

                    // Keep list from growing infinitely (holding min 10, max 20 measure points)
                    if (positions.length > 60) {
                        positions.splice(0, 30);
                    }

                    // Track scroll movement for decleration
                    positions.push(scrollLeft, scrollTop, timeStamp);

                    // Sync scroll position
                    self.__publish(scrollLeft, scrollTop, level);

                    // Otherwise figure out whether we are switching into dragging mode now.
                } else {

                    var minimumTrackingForScroll = self.options.locking ? 3 : 0;
                    var minimumTrackingForDrag = 5;

                    var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
                    var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);

                    self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
                    self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;

                    positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);

                    self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
                    if (self.__isDragging) {
                        self.__interruptedAnimation = false;
                        self.__fadeScrollbars('in');
                    }

                }

                // Update last touch positions and time stamp for next event
                self.__lastTouchLeft = currentTouchLeft;
                self.__lastTouchTop = currentTouchTop;
                self.__lastTouchMove = timeStamp;
                self.__lastScale = scale;

            },


            /**
             * Touch end handler for scrolling support
             */
            doTouchEnd: function (e, timeStamp) {
                if (timeStamp instanceof Date) {
                    timeStamp = timeStamp.valueOf();
                }
                if (typeof timeStamp !== "number") {
                    timeStamp = Date.now();
                }

                var self = this;

                // Ignore event when tracking is not enabled (no touchstart event on element)
                // This is required as this listener ('touchmove') sits on the document and not on the element itself.
                if (!self.__isTracking) {
                    return;
                }

                // Not touching anymore (when two finger hit the screen there are two touch end events)
                self.__isTracking = false;

                // Be sure to reset the dragging flag now. Here we also detect whether
                // the finger has moved fast enough to switch into a deceleration animation.
                if (self.__isDragging) {

                    // Reset dragging flag
                    self.__isDragging = false;

                    // Start deceleration
                    // Verify that the last move detected was in some relevant time frame
                    if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) {

                        // Then figure out what the scroll position was about 100ms ago
                        var positions = self.__positions;
                        var endPos = positions.length - 1;
                        var startPos = endPos;

                        // Move pointer to position measured 100ms ago
                        for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) {
                            startPos = i;
                        }

                        // If start and stop position is identical in a 100ms timeframe,
                        // we cannot compute any useful deceleration.
                        if (startPos !== endPos) {

                            // Compute relative movement between these two points
                            var timeOffset = positions[endPos] - positions[startPos];
                            var movedLeft = self.__scrollLeft - positions[startPos - 2];
                            var movedTop = self.__scrollTop - positions[startPos - 1];

                            // Based on 50ms compute the movement to apply for each render step
                            self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
                            self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);

                            // How much velocity is required to start the deceleration
                            var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? self.options.decelVelocityThresholdPaging : self.options.decelVelocityThreshold;

                            // Verify that we have enough velocity to start deceleration
                            if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {

                                // Deactivate pull-to-refresh when decelerating
                                if (!self.__refreshActive) {
                                    self.__startDeceleration(timeStamp);
                                }
                            }
                        } else {
                            self.__scrollingComplete();
                        }
                    } else if ((timeStamp - self.__lastTouchMove) > 100) {
                        self.__scrollingComplete();
                    }

                } else if (self.__decStopped) {
                    // the deceleration was stopped
                    // user flicked the scroll fast, and stop dragging, then did a touchstart to stop the srolling
                    // tell the touchend event code to do nothing, we don't want to actually send a click
                    e.isTapHandled = true;
                    self.__decStopped = false;
                }

                // If this was a slower move it is per default non decelerated, but this
                // still means that we want snap back to the bounds which is done here.
                // This is placed outside the condition above to improve edge case stability
                // e.g. touchend fired without enabled dragging. This should normally do not
                // have modified the scroll positions or even showed the scrollbars though.
                if (!self.__isDecelerating) {

                    if (self.__refreshActive && self.__refreshStart) {

                        // Use publish instead of scrollTo to allow scrolling to out of boundary position
                        // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
                        self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);

                        var d = new Date();
                        self.refreshStartTime = d.getTime();

                        if (self.__refreshStart) {
                            self.__refreshStart();
                        }
                        // for iOS-ey style scrolling
                        if (!ionic.Platform.isAndroid()) self.__startDeceleration();
                    } else {

                        if (self.__interruptedAnimation || self.__isDragging) {
                            self.__scrollingComplete();
                        }
                        self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);

                        // Directly signalize deactivation (nothing todo on refresh?)
                        if (self.__refreshActive) {

                            self.__refreshActive = false;
                            if (self.__refreshDeactivate) {
                                self.__refreshDeactivate();
                            }

                        }
                    }
                }

                // Fully cleanup list
                self.__positions.length = 0;

            },


            /*
             ---------------------------------------------------------------------------
             PRIVATE API
             ---------------------------------------------------------------------------
             */

            /**
             * Applies the scroll position to the content element
             *
             * @param left {Number} Left scroll position
             * @param top {Number} Top scroll position
             * @param animate {Boolean} Whether animation should be used to move to the new coordinates
             */
            __publish: function (left, top, zoom, animate, wasResize) {

                var self = this;

                // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
                var wasAnimating = self.__isAnimating;
                if (wasAnimating) {
                    zyngaCore.effect.Animate.stop(wasAnimating);
                    self.__isAnimating = false;
                }

                if (animate && self.options.animating) {

                    // Keep scheduled positions for scrollBy/zoomBy functionality
                    self.__scheduledLeft = left;
                    self.__scheduledTop = top;
                    self.__scheduledZoom = zoom;

                    var oldLeft = self.__scrollLeft;
                    var oldTop = self.__scrollTop;
                    var oldZoom = self.__zoomLevel;

                    var diffLeft = left - oldLeft;
                    var diffTop = top - oldTop;
                    var diffZoom = zoom - oldZoom;

                    var step = function (percent, now, render) {

                        if (render) {

                            self.__scrollLeft = oldLeft + (diffLeft * percent);
                            self.__scrollTop = oldTop + (diffTop * percent);
                            self.__zoomLevel = oldZoom + (diffZoom * percent);

                            // Push values out
                            if (self.__callback) {
                                self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel, wasResize);
                            }

                        }
                    };

                    var verify = function (id) {
                        return self.__isAnimating === id;
                    };

                    var completed = function (renderedFramesPerSecond, animationId, wasFinished) {
                        if (animationId === self.__isAnimating) {
                            self.__isAnimating = false;
                        }
                        if (self.__didDecelerationComplete || wasFinished) {
                            self.__scrollingComplete();
                        }

                        if (self.options.zooming) {
                            self.__computeScrollMax();
                        }
                    };

                    // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
                    self.__isAnimating = zyngaCore.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);

                } else {

                    self.__scheduledLeft = self.__scrollLeft = left;
                    self.__scheduledTop = self.__scrollTop = top;
                    self.__scheduledZoom = self.__zoomLevel = zoom;

                    // Push values out
                    if (self.__callback) {
                        self.__callback(left, top, zoom, wasResize);
                    }

                    // Fix max scroll ranges
                    if (self.options.zooming) {
                        self.__computeScrollMax();
                    }
                }
            },


            /**
             * Recomputes scroll minimum values based on client dimensions and content dimensions.
             */
            __computeScrollMax: function (zoomLevel) {
                var self = this;

                if (zoomLevel == null) {
                    zoomLevel = self.__zoomLevel;
                }

                self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0);
                self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0);

                if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
                    self.__didWaitForSize = true;
                    self.__waitForSize();
                }
            },


            /**
             * If the scroll view isn't sized correctly on start, wait until we have at least some size
             */
            __waitForSize: function () {
                var self = this;

                clearTimeout(self.__sizerTimeout);

                var sizer = function () {
                    self.resize(true);
                };

                sizer();
                self.__sizerTimeout = setTimeout(sizer, 500);
            },

            /*
             ---------------------------------------------------------------------------
             ANIMATION (DECELERATION) SUPPORT
             ---------------------------------------------------------------------------
             */

            /**
             * Called when a touch sequence end and the speed of the finger was high enough
             * to switch into deceleration mode.
             */
            __startDeceleration: function () {
                var self = this;

                if (self.options.paging) {

                    var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
                    var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
                    var clientWidth = self.__clientWidth;
                    var clientHeight = self.__clientHeight;

                    // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
                    // Each page should have exactly the size of the client area.
                    self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
                    self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
                    self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
                    self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;

                } else {

                    self.__minDecelerationScrollLeft = 0;
                    self.__minDecelerationScrollTop = 0;
                    self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
                    self.__maxDecelerationScrollTop = self.__maxScrollTop;
                    if (self.__refreshActive) self.__minDecelerationScrollTop = self.__refreshHeight * -1;
                }

                // Wrap class method
                var step = function (percent, now, render) {
                    self.__stepThroughDeceleration(render);
                };

                // How much velocity is required to keep the deceleration running
                self.__minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;

                // Detect whether it's still worth to continue animating steps
                // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
                var verify = function () {
                    var shouldContinue = Math.abs(self.__decelerationVelocityX) >= self.__minVelocityToKeepDecelerating ||
                        Math.abs(self.__decelerationVelocityY) >= self.__minVelocityToKeepDecelerating;
                    if (!shouldContinue) {
                        self.__didDecelerationComplete = true;

                        //Make sure the scroll values are within the boundaries after a bounce,
                        //not below 0 or above maximum
                        if (self.options.bouncing && !self.__refreshActive) {
                            self.scrollTo(
                                Math.min(Math.max(self.__scrollLeft, 0), self.__maxScrollLeft),
                                Math.min(Math.max(self.__scrollTop, 0), self.__maxScrollTop),
                                self.__refreshActive
                            );
                        }
                    }
                    return shouldContinue;
                };

                var completed = function () {
                    self.__isDecelerating = false;
                    if (self.__didDecelerationComplete) {
                        self.__scrollingComplete();
                    }

                    // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
                    if (self.options.paging) {
                        self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
                    }
                };

                // Start animation and switch on flag
                self.__isDecelerating = zyngaCore.effect.Animate.start(step, verify, completed);

            },


            /**
             * Called on every step of the animation
             *
             * @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only!
             */
            __stepThroughDeceleration: function (render) {
                var self = this;


                //
                // COMPUTE NEXT SCROLL POSITION
                //

                // Add deceleration to scroll position
                var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;// * self.options.deceleration);
                var scrollTop = self.__scrollTop + self.__decelerationVelocityY;// * self.options.deceleration);


                //
                // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
                //

                if (!self.options.bouncing) {

                    var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
                    if (scrollLeftFixed !== scrollLeft) {
                        scrollLeft = scrollLeftFixed;
                        self.__decelerationVelocityX = 0;
                    }

                    var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
                    if (scrollTopFixed !== scrollTop) {
                        scrollTop = scrollTopFixed;
                        self.__decelerationVelocityY = 0;
                    }

                }


                //
                // UPDATE SCROLL POSITION
                //

                if (render) {

                    self.__publish(scrollLeft, scrollTop, self.__zoomLevel);

                } else {

                    self.__scrollLeft = scrollLeft;
                    self.__scrollTop = scrollTop;

                }


                //
                // SLOW DOWN
                //

                // Slow down velocity on every iteration
                if (!self.options.paging) {

                    // This is the factor applied to every iteration of the animation
                    // to slow down the process. This should emulate natural behavior where
                    // objects slow down when the initiator of the movement is removed
                    var frictionFactor = self.options.deceleration;

                    self.__decelerationVelocityX *= frictionFactor;
                    self.__decelerationVelocityY *= frictionFactor;

                }


                //
                // BOUNCING SUPPORT
                //

                if (self.options.bouncing) {

                    var scrollOutsideX = 0;
                    var scrollOutsideY = 0;

                    // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
                    var penetrationDeceleration = self.options.penetrationDeceleration;
                    var penetrationAcceleration = self.options.penetrationAcceleration;

                    // Check limits
                    if (scrollLeft < self.__minDecelerationScrollLeft) {
                        scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
                    } else if (scrollLeft > self.__maxDecelerationScrollLeft) {
                        scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
                    }

                    if (scrollTop < self.__minDecelerationScrollTop) {
                        scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
                    } else if (scrollTop > self.__maxDecelerationScrollTop) {
                        scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
                    }

                    // Slow down until slow enough, then flip back to snap position
                    if (scrollOutsideX !== 0) {
                        var isHeadingOutwardsX = scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft;
                        if (isHeadingOutwardsX) {
                            self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
                        }
                        var isStoppedX = Math.abs(self.__decelerationVelocityX) <= self.__minVelocityToKeepDecelerating;
                        //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
                        if (!isHeadingOutwardsX || isStoppedX) {
                            self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
                        }
                    }

                    if (scrollOutsideY !== 0) {
                        var isHeadingOutwardsY = scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop;
                        if (isHeadingOutwardsY) {
                            self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
                        }
                        var isStoppedY = Math.abs(self.__decelerationVelocityY) <= self.__minVelocityToKeepDecelerating;
                        //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds
                        if (!isHeadingOutwardsY || isStoppedY) {
                            self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
                        }
                    }
                }
            },


            /**
             * calculate the distance between two touches
             * @param   {Touch}     touch1
             * @param   {Touch}     touch2
             * @returns {Number}    distance
             */
            __getDistance: function getDistance(touch1, touch2) {
                var x = touch2.pageX - touch1.pageX,
                    y = touch2.pageY - touch1.pageY;
                return Math.sqrt((x * x) + (y * y));
            },


            /**
             * calculate the scale factor between two touchLists (fingers)
             * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
             * @param   {Array}     start
             * @param   {Array}     end
             * @returns {Number}    scale
             */
            __getScale: function getScale(start, end) {
                // need two fingers...
                if (start.length >= 2 && end.length >= 2) {
                    return this.__getDistance(end[0], end[1]) /
                        this.__getDistance(start[0], start[1]);
                }
                return 1;
            }
        });

        ionic.scroll = {
            isScrolling: false,
            lastTop: 0
        };

    })(ionic);

    (function (ionic) {
        var NOOP = function () {
        };
        var deprecated = function (name) {
            void 0;
        };
        ionic.views.ScrollNative = ionic.views.View.inherit({

            initialize: function (options) {
                var self = this;
                self.__container = self.el = options.el;
                self.__content = options.el.firstElementChild;
                // Whether scrolling is frozen or not
                self.__frozen = false;
                self.isNative = true;

                self.__scrollTop = self.el.scrollTop;
                self.__scrollLeft = self.el.scrollLeft;
                self.__clientHeight = self.__content.clientHeight;
                self.__clientWidth = self.__content.clientWidth;
                self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);
                self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);

                if (options.startY >= 0 || options.startX >= 0) {
                    ionic.requestAnimationFrame(function () {
                        self.__originalContainerHeight = self.el.getBoundingClientRect().height;

                        self.el.scrollTop = options.startY || 0;
                        self.el.scrollLeft = options.startX || 0;

                        self.__scrollTop = self.el.scrollTop;
                        self.__scrollLeft = self.el.scrollLeft;
                    });
                }

                self.options = {

                    freeze: false,

                    getContentWidth: function () {
                        return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
                    },

                    getContentHeight: function () {
                        return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
                    }

                };

                for (var key in options) {
                    self.options[key] = options[key];
                }

                /**
                 * Sets isScrolling to true, and automatically deactivates if not called again in 80ms.
                 */
                self.onScroll = function () {
                    if (!ionic.scroll.isScrolling) {
                        ionic.scroll.isScrolling = true;
                    }

                    clearTimeout(self.scrollTimer);
                    self.scrollTimer = setTimeout(function () {
                        ionic.scroll.isScrolling = false;
                    }, 80);
                };

                self.freeze = function (shouldFreeze) {
                    self.__frozen = shouldFreeze;
                };
                // A more powerful freeze pop that dominates all other freeze pops
                self.freezeShut = function (shouldFreezeShut) {
                    self.__frozenShut = shouldFreezeShut;
                };

                self.__initEventHandlers();
            },

            /**  Methods not used in native scrolling */
            __callback: function () {
                deprecated('__callback');
            },
            zoomTo: function () {
                deprecated('zoomTo');
            },
            zoomBy: function () {
                deprecated('zoomBy');
            },
            activatePullToRefresh: function () {
                deprecated('activatePullToRefresh');
            },

            /**
             * Returns the scroll position and zooming values
             *
             * @return {Map} `left` and `top` scroll position and `zoom` level
             */
            resize: function (continueScrolling) {
                var self = this;
                if (!self.__container || !self.options) return;

                // Update Scroller dimensions for changed content
                // Add padding to bottom of content
                self.setDimensions(
                    self.__container.clientWidth,
                    self.__container.clientHeight,
                    self.options.getContentWidth(),
                    self.options.getContentHeight(),
                    continueScrolling
                );
            },

            /**
             * Initialize the scrollview
             * In native scrolling, this only means we need to gather size information
             */
            run: function () {
                this.resize();
            },

            /**
             * Returns the scroll position and zooming values
             *
             * @return {Map} `left` and `top` scroll position and `zoom` level
             */
            getValues: function () {
                var self = this;
                self.update();
                return {
                    left: self.__scrollLeft,
                    top: self.__scrollTop,
                    zoom: 1
                };
            },

            /**
             * Updates the __scrollLeft and __scrollTop values to el's current value
             */
            update: function () {
                var self = this;
                self.__scrollLeft = self.el.scrollLeft;
                self.__scrollTop = self.el.scrollTop;
            },

            /**
             * Configures the dimensions of the client (outer) and content (inner) elements.
             * Requires the available space for the outer element and the outer size of the inner element.
             * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
             *
             * @param clientWidth {Integer} Inner width of outer element
             * @param clientHeight {Integer} Inner height of outer element
             * @param contentWidth {Integer} Outer width of inner element
             * @param contentHeight {Integer} Outer height of inner element
             */
            setDimensions: function (clientWidth, clientHeight, contentWidth, contentHeight) {
                var self = this;

                if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
                    // this scrollview isn't rendered, don't bother
                    return;
                }

                // Only update values which are defined
                if (clientWidth === +clientWidth) {
                    self.__clientWidth = clientWidth;
                }

                if (clientHeight === +clientHeight) {
                    self.__clientHeight = clientHeight;
                }

                if (contentWidth === +contentWidth) {
                    self.__contentWidth = contentWidth;
                }

                if (contentHeight === +contentHeight) {
                    self.__contentHeight = contentHeight;
                }

                // Refresh maximums
                self.__computeScrollMax();
            },

            /**
             * Returns the maximum scroll values
             *
             * @return {Map} `left` and `top` maximum scroll values
             */
            getScrollMax: function () {
                return {
                    left: this.__maxScrollLeft,
                    top: this.__maxScrollTop
                };
            },

            /**
             * Scrolls by the given amount in px.
             *
             * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
             * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
             * @param animate {Boolean} Whether the scrolling should happen using an animation
             */

            scrollBy: function (left, top, animate) {
                var self = this;

                // update scroll vars before refferencing them
                self.update();

                var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
                var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;

                self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
            },

            /**
             * Scrolls to the given position in px.
             *
             * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code>
             * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code>
             * @param animate {Boolean} Whether the scrolling should happen using an animation
             */
            scrollTo: function (left, top, animate) {
                var self = this;
                if (!animate) {
                    self.el.scrollTop = top;
                    self.el.scrollLeft = left;
                    self.resize();
                    return;
                }

                var oldOverflowX = self.el.style.overflowX;
                var oldOverflowY = self.el.style.overflowY;

                clearTimeout(self.__scrollToCleanupTimeout);
                self.__scrollToCleanupTimeout = setTimeout(function () {
                    self.el.style.overflowX = oldOverflowX;
                    self.el.style.overflowY = oldOverflowY;
                }, 500);

                self.el.style.overflowY = 'hidden';
                self.el.style.overflowX = 'hidden';

                animateScroll(top, left);

                function animateScroll(Y, X) {
                    // scroll animation loop w/ easing
                    // credit https://gist.github.com/dezinezync/5487119
                    var start = Date.now(),
                        duration = 250, //milliseconds
                        fromY = self.el.scrollTop,
                        fromX = self.el.scrollLeft;

                    if (fromY === Y && fromX === X) {
                        self.el.style.overflowX = oldOverflowX;
                        self.el.style.overflowY = oldOverflowY;
                        self.resize();
                        return;
                        /* Prevent scrolling to the Y point if already there */
                    }

                    // decelerating to zero velocity
                    function easeOutCubic(t) {
                        return (--t) * t * t + 1;
                    }

                    // scroll loop
                    function animateScrollStep() {
                        var currentTime = Date.now(),
                            time = Math.min(1, ((currentTime - start) / duration)),
                            // where .5 would be 50% of time on a linear scale easedT gives a
                            // fraction based on the easing method
                            easedT = easeOutCubic(time);

                        if (fromY != Y) {
                            self.el.scrollTop = parseInt((easedT * (Y - fromY)) + fromY, 10);
                        }
                        if (fromX != X) {
                            self.el.scrollLeft = parseInt((easedT * (X - fromX)) + fromX, 10);
                        }

                        if (time < 1) {
                            ionic.requestAnimationFrame(animateScrollStep);

                        } else {
                            // done
                            ionic.tap.removeClonedInputs(self.__container, self);
                            self.el.style.overflowX = oldOverflowX;
                            self.el.style.overflowY = oldOverflowY;
                            self.resize();
                        }
                    }

                    // start scroll loop
                    ionic.requestAnimationFrame(animateScrollStep);
                }
            },


            /*
             ---------------------------------------------------------------------------
             PRIVATE API
             ---------------------------------------------------------------------------
             */

            /**
             * If the scroll view isn't sized correctly on start, wait until we have at least some size
             */
            __waitForSize: function () {
                var self = this;

                clearTimeout(self.__sizerTimeout);

                var sizer = function () {
                    self.resize(true);
                };

                sizer();
                self.__sizerTimeout = setTimeout(sizer, 500);
            },


            /**
             * Recomputes scroll minimum values based on client dimensions and content dimensions.
             */
            __computeScrollMax: function () {
                var self = this;

                self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);
                self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);

                if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
                    self.__didWaitForSize = true;
                    self.__waitForSize();
                }
            },

            __initEventHandlers: function () {
                var self = this;

                // Event Handler
                var container = self.__container;
                // save height when scroll view is shrunk so we don't need to reflow
                var scrollViewOffsetHeight;

                var lastKeyboardHeight;

                /**
                 * Shrink the scroll view when the keyboard is up if necessary and if the
                 * focused input is below the bottom of the shrunk scroll view, scroll it
                 * into view.
                 */
                self.scrollChildIntoView = function (e) {
                    var rect = container.getBoundingClientRect();
                    if (!self.__originalContainerHeight) {
                        self.__originalContainerHeight = rect.height;
                    }

                    // D
                    //var scrollBottomOffsetToTop = rect.bottom;
                    // D - A
                    scrollViewOffsetHeight = self.__originalContainerHeight;
                    //console.log('Scroll view offset height', scrollViewOffsetHeight);
                    //console.dir(container);
                    var alreadyShrunk = self.isShrunkForKeyboard;

                    var isModal = container.parentNode.classList.contains('modal');
                    var isPopover = container.parentNode.classList.contains('popover');
                    // 680px is when the media query for 60% modal width kicks in
                    var isInsetModal = isModal && window.innerWidth >= 680;

                    /*
                     *  _______
                     * |---A---| <- top of scroll view
                     * |       |
                     * |---B---| <- keyboard
                     * |   C   | <- input
                     * |---D---| <- initial bottom of scroll view
                     * |___E___| <- bottom of viewport
                     *
                     *  All commented calculations relative to the top of the viewport (ie E
                     *  is the viewport height, not 0)
                     */


                    var changedKeyboardHeight = lastKeyboardHeight && (lastKeyboardHeight !== e.detail.keyboardHeight);

                    if (!alreadyShrunk || changedKeyboardHeight) {
                        // shrink scrollview so we can actually scroll if the input is hidden
                        // if it isn't shrink so we can scroll to inputs under the keyboard
                        // inset modals won't shrink on Android on their own when the keyboard appears
                        if (!isPopover && (ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal)) {
                            // if there are things below the scroll view account for them and
                            // subtract them from the keyboard height when resizing
                            // E - D                         E                         D
                            //var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;

                            // 0 or D - B if D > B           E - B                     E - D
                            //var keyboardOffset = e.detail.keyboardHeight - scrollBottomOffsetToBottom;

                            ionic.requestAnimationFrame(function () {
                                // D - A or B - A if D > B       D - A             max(0, D - B)
                                scrollViewOffsetHeight = Math.max(0, Math.min(self.__originalContainerHeight, self.__originalContainerHeight - (e.detail.keyboardHeight - 43)));//keyboardOffset >= 0 ? scrollViewOffsetHeight - keyboardOffset : scrollViewOffsetHeight + keyboardOffset;

                                //console.log('Old container height', self.__originalContainerHeight, 'New container height', scrollViewOffsetHeight, 'Keyboard height', e.detail.keyboardHeight);

                                container.style.height = scrollViewOffsetHeight + "px";

                                /*
                                 if (ionic.Platform.isIOS()) {
                                 // Force redraw to avoid disappearing content
                                 var disp = container.style.display;
                                 container.style.display = 'none';
                                 var trick = container.offsetHeight;
                                 container.style.display = disp;
                                 }
                                 */
                                container.classList.add('keyboard-up');
                                //update scroll view
                                self.resize();
                            });
                        }

                        self.isShrunkForKeyboard = true;
                    }

                    lastKeyboardHeight = e.detail.keyboardHeight;

                    /*
                     *  _______
                     * |---A---| <- top of scroll view
                     * |   *   | <- where we want to scroll to
                     * |--B-D--| <- keyboard, bottom of scroll view
                     * |   C   | <- input
                     * |       |
                     * |___E___| <- bottom of viewport
                     *
                     *  All commented calculations relative to the top of the viewport (ie E
                     *  is the viewport height, not 0)
                     */
                    // if the element is positioned under the keyboard scroll it into view
                    if (e.detail.isElementUnderKeyboard) {

                        ionic.requestAnimationFrame(function () {
                            var pos = ionic.DomUtil.getOffsetTop(e.detail.target);
                            setTimeout(function () {
                                if (ionic.Platform.isIOS()) {
                                    ionic.tap.cloneFocusedInput(container, self);
                                }
                                // Scroll the input into view, with a 100px buffer
                                self.scrollTo(0, pos - (rect.top + 100), true);
                                self.onScroll();
                            }, 32);

                            /*
                             // update D if we shrunk
                             if (self.isShrunkForKeyboard && !alreadyShrunk) {
                             scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
                             console.log('Scroll bottom', scrollBottomOffsetToTop);
                             }

                             // middle of the scrollview, this is where we want to scroll to
                             // (D - A) / 2
                             var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
                             console.log('Midpoint', scrollMidpointOffset);
                             //console.log("container.offsetHeight: " + scrollViewOffsetHeight);

                             // middle of the input we want to scroll into view
                             // C
                             var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);
                             console.log('Input midpoint');

                             // distance from middle of input to the bottom of the scroll view
                             // C - D                                C               D
                             var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;
                             console.log('Input midpoint offset', inputMidpointOffsetToScrollBottom);

                             //C - D + (D - A)/2          C - D                     (D - A)/ 2
                             var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;
                             console.log('Scroll top', scrollTop);

                             if ( scrollTop > 0) {
                             if (ionic.Platform.isIOS()) {
                             //just shrank scroll view, give it some breathing room before scrolling
                             setTimeout(function(){
                             ionic.tap.cloneFocusedInput(container, self);
                             self.scrollBy(0, scrollTop, true);
                             self.onScroll();
                             }, 32);
                             } else {
                             self.scrollBy(0, scrollTop, true);
                             self.onScroll();
                             }
                             }
                             */
                        });
                    }

                    // Only the first scrollView parent of the element that broadcasted this event
                    // (the active element that needs to be shown) should receive this event
                    e.stopPropagation();
                };

                self.resetScrollView = function () {
                    //return scrollview to original height once keyboard has hidden
                    if (self.isShrunkForKeyboard) {
                        self.isShrunkForKeyboard = false;
                        container.style.height = "";

                        /*
                         if (ionic.Platform.isIOS()) {
                         // Force redraw to avoid disappearing content
                         var disp = container.style.display;
                         container.style.display = 'none';
                         var trick = container.offsetHeight;
                         container.style.display = disp;
                         }
                         */

                        self.__originalContainerHeight = container.getBoundingClientRect().height;

                        if (ionic.Platform.isIOS()) {
                            ionic.requestAnimationFrame(function () {
                                container.classList.remove('keyboard-up');
                            });
                        }

                    }
                    self.resize();
                };

                self.handleTouchMove = function (e) {
                    if (self.__frozenShut) {
                        e.preventDefault();
                        e.stopPropagation();
                        return false;

                    } else if (self.__frozen) {
                        e.preventDefault();
                        // let it propagate so other events such as drag events can happen,
                        // but don't let it actually scroll
                        return false;
                    }
                    return true;
                };

                container.addEventListener('scroll', self.onScroll);

                //Broadcasted when keyboard is shown on some platforms.
                //See js/utils/keyboard.js
                container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);

                container.addEventListener(ionic.EVENTS.touchstart, self.handleTouchMove);
                container.addEventListener(ionic.EVENTS.touchmove, self.handleTouchMove);

                // Listen on document because container may not have had the last
                // keyboardActiveElement, for example after closing a modal with a focused
                // input and returning to a previously resized scroll view in an ion-content.
                // Since we can only resize scroll views that are currently visible, just resize
                // the current scroll view when the keyboard is closed.
                document.addEventListener('resetScrollView', self.resetScrollView);
            },

            __cleanup: function () {
                var self = this;
                var container = self.__container;

                container.removeEventListener('scroll', self.onScroll);
                container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);

                container.removeEventListener(ionic.EVENTS.touchstart, self.handleTouchMove);
                container.removeEventListener(ionic.EVENTS.touchmove, self.handleTouchMove);

                document.removeEventListener('resetScrollView', self.resetScrollView);

                ionic.tap.removeClonedInputs(container, self);

                delete self.__container;
                delete self.__content;
                delete self.__indicatorX;
                delete self.__indicatorY;
                delete self.options.el;

                self.resize = self.scrollTo = self.onScroll = self.resetScrollView = NOOP;
                self.scrollChildIntoView = NOOP;
                container = null;
            }
        });

    })(ionic);

    (function (ionic) {
        'use strict';

        var ITEM_CLASS = 'item';
        var ITEM_CONTENT_CLASS = 'item-content';
        var ITEM_SLIDING_CLASS = 'item-sliding';
        var ITEM_OPTIONS_CLASS = 'item-options';
        var ITEM_PLACEHOLDER_CLASS = 'item-placeholder';
        var ITEM_REORDERING_CLASS = 'item-reordering';
        var ITEM_REORDER_BTN_CLASS = 'item-reorder';

        var DragOp = function () {
        };
        DragOp.prototype = {
            start: function () {
            },
            drag: function () {
            },
            end: function () {
            },
            isSameItem: function () {
                return false;
            }
        };

        var SlideDrag = function (opts) {
            this.dragThresholdX = opts.dragThresholdX || 10;
            this.el = opts.el;
            this.item = opts.item;
            this.canSwipe = opts.canSwipe;
        };

        SlideDrag.prototype = new DragOp();

        SlideDrag.prototype.start = function (e) {
            var content, buttons, offsetX, buttonsWidth;

            if (!this.canSwipe()) {
                return;
            }

            if (e.target.classList.contains(ITEM_CONTENT_CLASS)) {
                content = e.target;
            } else if (e.target.classList.contains(ITEM_CLASS)) {
                content = e.target.querySelector('.' + ITEM_CONTENT_CLASS);
            } else {
                content = ionic.DomUtil.getParentWithClass(e.target, ITEM_CONTENT_CLASS);
            }

            // If we don't have a content area as one of our children (or ourselves), skip
            if (!content) {
                return;
            }

            // Make sure we aren't animating as we slide
            content.classList.remove(ITEM_SLIDING_CLASS);

            // Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start)
            offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0;

            // Grab the buttons
            buttons = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS);
            if (!buttons) {
                return;
            }
            buttons.classList.remove('invisible');

            buttonsWidth = buttons.offsetWidth;

            this._currentDrag = {
                buttons: buttons,
                buttonsWidth: buttonsWidth,
                content: content,
                startOffsetX: offsetX
            };
        };

        /**
         * Check if this is the same item that was previously dragged.
         */
        SlideDrag.prototype.isSameItem = function (op) {
            if (op._lastDrag && this._currentDrag) {
                return this._currentDrag.content == op._lastDrag.content;
            }
            return false;
        };

        SlideDrag.prototype.clean = function (isInstant) {
            var lastDrag = this._lastDrag;

            if (!lastDrag || !lastDrag.content) return;

            lastDrag.content.style[ionic.CSS.TRANSITION] = '';
            lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
            if (isInstant) {
                lastDrag.content.style[ionic.CSS.TRANSITION] = 'none';
                makeInvisible();
                ionic.requestAnimationFrame(function () {
                    lastDrag.content.style[ionic.CSS.TRANSITION] = '';
                });
            } else {
                ionic.requestAnimationFrame(function () {
                    setTimeout(makeInvisible, 250);
                });
            }
            function makeInvisible() {
                lastDrag.buttons && lastDrag.buttons.classList.add('invisible');
            }
        };

        SlideDrag.prototype.drag = ionic.animationFrameThrottle(function (e) {
            var buttonsWidth;

            // We really aren't dragging
            if (!this._currentDrag) {
                return;
            }

            // Check if we should start dragging. Check if we've dragged past the threshold,
            // or we are starting from the open state.
            if (!this._isDragging &&
                ((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
                (Math.abs(this._currentDrag.startOffsetX) > 0))) {
                this._isDragging = true;
            }

            if (this._isDragging) {
                buttonsWidth = this._currentDrag.buttonsWidth;

                // Grab the new X point, capping it at zero
                var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);

                // If the new X position is past the buttons, we need to slow down the drag (rubber band style)
                if (newX < -buttonsWidth) {
                    // Calculate the new X position, capped at the top of the buttons
                    newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
                }

                this._currentDrag.content.$$ionicOptionsOpen = newX !== 0;

                this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
                this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
            }
        });

        SlideDrag.prototype.end = function (e, doneCallback) {
            var self = this;

            // There is no drag, just end immediately
            if (!self._currentDrag) {
                doneCallback && doneCallback();
                return;
            }

            // If we are currently dragging, we want to snap back into place
            // The final resting point X will be the width of the exposed buttons
            var restingPoint = -self._currentDrag.buttonsWidth;

            // Check if the drag didn't clear the buttons mid-point
            // and we aren't moving fast enough to swipe open
            if (e.gesture.deltaX > -(self._currentDrag.buttonsWidth / 2)) {

                // If we are going left but too slow, or going right, go back to resting
                if (e.gesture.direction == "left" && Math.abs(e.gesture.velocityX) < 0.3) {
                    restingPoint = 0;

                } else if (e.gesture.direction == "right") {
                    restingPoint = 0;
                }

            }

            ionic.requestAnimationFrame(function () {
                if (restingPoint === 0) {
                    self._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
                    var buttons = self._currentDrag.buttons;
                    setTimeout(function () {
                        buttons && buttons.classList.add('invisible');
                    }, 250);
                } else {
                    self._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px,0,0)';
                }
                self._currentDrag.content.style[ionic.CSS.TRANSITION] = '';


                // Kill the current drag
                if (!self._lastDrag) {
                    self._lastDrag = {};
                }
                ionic.extend(self._lastDrag, self._currentDrag);
                if (self._currentDrag) {
                    self._currentDrag.buttons = null;
                    self._currentDrag.content = null;
                }
                self._currentDrag = null;

                // We are done, notify caller
                doneCallback && doneCallback();
            });
        };

        var ReorderDrag = function (opts) {
            var self = this;

            self.dragThresholdY = opts.dragThresholdY || 0;
            self.onReorder = opts.onReorder;
            self.listEl = opts.listEl;
            self.el = self.item = opts.el;
            self.scrollEl = opts.scrollEl;
            self.scrollView = opts.scrollView;
            // Get the True Top of the list el http://www.quirksmode.org/js/findpos.html
            self.listElTrueTop = 0;
            if (self.listEl.offsetParent) {
                var obj = self.listEl;
                do {
                    self.listElTrueTop += obj.offsetTop;
                    obj = obj.offsetParent;
                } while (obj);
            }
        };

        ReorderDrag.prototype = new DragOp();

        ReorderDrag.prototype._moveElement = function (e) {
            var y = e.gesture.center.pageY +
                this.scrollView.getValues().top -
                (this._currentDrag.elementHeight / 2) -
                this.listElTrueTop;
            this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(0, ' + y + 'px, 0)';
        };

        ReorderDrag.prototype.deregister = function () {
            this.listEl = this.el = this.scrollEl = this.scrollView = null;
        };

        ReorderDrag.prototype.start = function (e) {

            var startIndex = ionic.DomUtil.getChildIndex(this.el, this.el.nodeName.toLowerCase());
            var elementHeight = this.el.scrollHeight;
            var placeholder = this.el.cloneNode(true);

            placeholder.classList.add(ITEM_PLACEHOLDER_CLASS);

            this.el.parentNode.insertBefore(placeholder, this.el);
            this.el.classList.add(ITEM_REORDERING_CLASS);

            this._currentDrag = {
                elementHeight: elementHeight,
                startIndex: startIndex,
                placeholder: placeholder,
                scrollHeight: scroll,
                list: placeholder.parentNode
            };

            this._moveElement(e);
        };

        ReorderDrag.prototype.drag = ionic.animationFrameThrottle(function (e) {
            // We really aren't dragging
            var self = this;
            if (!this._currentDrag) {
                return;
            }

            var scrollY = 0;
            var pageY = e.gesture.center.pageY;
            var offset = this.listElTrueTop;

            //If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary
            if (this.scrollView) {

                var container = this.scrollView.__container;
                scrollY = this.scrollView.getValues().top;

                var containerTop = container.offsetTop;
                var pixelsPastTop = containerTop - pageY + this._currentDrag.elementHeight / 2;
                var pixelsPastBottom = pageY + this._currentDrag.elementHeight / 2 - containerTop - container.offsetHeight;

                if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) {
                    this.scrollView.scrollBy(null, -pixelsPastTop);
                    //Trigger another drag so the scrolling keeps going
                    ionic.requestAnimationFrame(function () {
                        self.drag(e);
                    });
                }
                if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) {
                    if (scrollY < this.scrollView.getScrollMax().top) {
                        this.scrollView.scrollBy(null, pixelsPastBottom);
                        //Trigger another drag so the scrolling keeps going
                        ionic.requestAnimationFrame(function () {
                            self.drag(e);
                        });
                    }
                }
            }

            // Check if we should start dragging. Check if we've dragged past the threshold,
            // or we are starting from the open state.
            if (!this._isDragging && Math.abs(e.gesture.deltaY) > this.dragThresholdY) {
                this._isDragging = true;
            }

            if (this._isDragging) {
                this._moveElement(e);

                this._currentDrag.currentY = scrollY + pageY - offset;

                // this._reorderItems();
            }
        });

        // When an item is dragged, we need to reorder any items for sorting purposes
        ReorderDrag.prototype._getReorderIndex = function () {
            var self = this;

            var siblings = Array.prototype.slice.call(self._currentDrag.placeholder.parentNode.children)
                .filter(function (el) {
                    return el.nodeName === self.el.nodeName && el !== self.el;
                });

            var dragOffsetTop = self._currentDrag.currentY;
            var el;
            for (var i = 0, len = siblings.length; i < len; i++) {
                el = siblings[i];
                if (i === len - 1) {
                    if (dragOffsetTop > el.offsetTop) {
                        return i;
                    }
                } else if (i === 0) {
                    if (dragOffsetTop < el.offsetTop + el.offsetHeight) {
                        return i;
                    }
                } else if (dragOffsetTop > el.offsetTop - el.offsetHeight / 2 &&
                    dragOffsetTop < el.offsetTop + el.offsetHeight) {
                    return i;
                }
            }
            return self._currentDrag.startIndex;
        };

        ReorderDrag.prototype.end = function (e, doneCallback) {
            if (!this._currentDrag) {
                doneCallback && doneCallback();
                return;
            }

            var placeholder = this._currentDrag.placeholder;
            var finalIndex = this._getReorderIndex();

            // Reposition the element
            this.el.classList.remove(ITEM_REORDERING_CLASS);
            this.el.style[ionic.CSS.TRANSFORM] = '';

            placeholder.parentNode.insertBefore(this.el, placeholder);
            placeholder.parentNode.removeChild(placeholder);

            this.onReorder && this.onReorder(this.el, this._currentDrag.startIndex, finalIndex);

            this._currentDrag = {
                placeholder: null,
                content: null
            };
            this._currentDrag = null;
            doneCallback && doneCallback();
        };


        /**
         * The ListView handles a list of items. It will process drag animations, edit mode,
         * and other operations that are common on mobile lists or table views.
         */
        ionic.views.ListView = ionic.views.View.inherit({
            initialize: function (opts) {
                var self = this;

                opts = ionic.extend({
                    onReorder: function () {
                    },
                    virtualRemoveThreshold: -200,
                    virtualAddThreshold: 200,
                    canSwipe: function () {
                        return true;
                    }
                }, opts);

                ionic.extend(self, opts);

                if (!self.itemHeight && self.listEl) {
                    self.itemHeight = self.listEl.children[0] && parseInt(self.listEl.children[0].style.height, 10);
                }

                self.onRefresh = opts.onRefresh || function () {
                    };
                self.onRefreshOpening = opts.onRefreshOpening || function () {
                    };
                self.onRefreshHolding = opts.onRefreshHolding || function () {
                    };

                var gestureOpts = {};
                // don't prevent native scrolling
                if (ionic.DomUtil.getParentOrSelfWithClass(self.el, 'overflow-scroll')) {
                    gestureOpts.prevent_default_directions = ['left', 'right'];
                }

                window.ionic.onGesture('release', function (e) {
                    self._handleEndDrag(e);
                }, self.el, gestureOpts);

                window.ionic.onGesture('drag', function (e) {
                    self._handleDrag(e);
                }, self.el, gestureOpts);
                // Start the drag states
                self._initDrag();
            },

            /**
             * Be sure to cleanup references.
             */
            deregister: function () {
                this.el = this.listEl = this.scrollEl = this.scrollView = null;

                // ensure no scrolls have been left frozen
                if (this.isScrollFreeze) {
                    self.scrollView.freeze(false);
                }
            },

            /**
             * Called to tell the list to stop refreshing. This is useful
             * if you are refreshing the list and are done with refreshing.
             */
            stopRefreshing: function () {
                var refresher = this.el.querySelector('.list-refresher');
                refresher.style.height = '0';
            },

            /**
             * If we scrolled and have virtual mode enabled, compute the window
             * of active elements in order to figure out the viewport to render.
             */
            didScroll: function (e) {
                var self = this;

                if (self.isVirtual) {
                    var itemHeight = self.itemHeight;

                    // Grab the total height of the list
                    var scrollHeight = e.target.scrollHeight;

                    // Get the viewport height
                    var viewportHeight = self.el.parentNode.offsetHeight;

                    // High water is the pixel position of the first element to include (everything before
                    // that will be removed)
                    var highWater = Math.max(0, e.scrollTop + self.virtualRemoveThreshold);

                    // Low water is the pixel position of the last element to include (everything after
                    // that will be removed)
                    var lowWater = Math.min(scrollHeight, Math.abs(e.scrollTop) + viewportHeight + self.virtualAddThreshold);

                    // Get the first and last elements in the list based on how many can fit
                    // between the pixel range of lowWater and highWater
                    var first = parseInt(Math.abs(highWater / itemHeight), 10);
                    var last = parseInt(Math.abs(lowWater / itemHeight), 10);

                    // Get the items we need to remove
                    self._virtualItemsToRemove = Array.prototype.slice.call(self.listEl.children, 0, first);

                    self.renderViewport && self.renderViewport(highWater, lowWater, first, last);
                }
            },

            didStopScrolling: function () {
                if (this.isVirtual) {
                    for (var i = 0; i < this._virtualItemsToRemove.length; i++) {
                        //el.parentNode.removeChild(el);
                        this.didHideItem && this.didHideItem(i);
                    }
                    // Once scrolling stops, check if we need to remove old items

                }
            },

            /**
             * Clear any active drag effects on the list.
             */
            clearDragEffects: function (isInstant) {
                if (this._lastDragOp) {
                    this._lastDragOp.clean && this._lastDragOp.clean(isInstant);
                    this._lastDragOp.deregister && this._lastDragOp.deregister();
                    this._lastDragOp = null;
                }
            },

            _initDrag: function () {
                // Store the last one
                if (this._lastDragOp) {
                    this._lastDragOp.deregister && this._lastDragOp.deregister();
                }
                this._lastDragOp = this._dragOp;

                this._dragOp = null;
            },

            // Return the list item from the given target
            _getItem: function (target) {
                while (target) {
                    if (target.classList && target.classList.contains(ITEM_CLASS)) {
                        return target;
                    }
                    target = target.parentNode;
                }
                return null;
            },


            _startDrag: function (e) {
                var self = this;

                self._isDragging = false;

                var lastDragOp = self._lastDragOp;
                var item;

                // If we have an open SlideDrag and we're scrolling the list. Clear it.
                if (self._didDragUpOrDown && lastDragOp instanceof SlideDrag) {
                    lastDragOp.clean && lastDragOp.clean();
                }

                // Check if this is a reorder drag
                if (ionic.DomUtil.getParentOrSelfWithClass(e.target, ITEM_REORDER_BTN_CLASS) && (e.gesture.direction == 'up' || e.gesture.direction == 'down')) {
                    item = self._getItem(e.target);

                    if (item) {
                        self._dragOp = new ReorderDrag({
                            listEl: self.el,
                            el: item,
                            scrollEl: self.scrollEl,
                            scrollView: self.scrollView,
                            onReorder: function (el, start, end) {
                                self.onReorder && self.onReorder(el, start, end);
                            }
                        });
                        self._dragOp.start(e);
                        e.preventDefault();
                    }
                }

                // Or check if this is a swipe to the side drag
                else if (!self._didDragUpOrDown && (e.gesture.direction == 'left' || e.gesture.direction == 'right') && Math.abs(e.gesture.deltaX) > 5) {

                    // Make sure this is an item with buttons
                    item = self._getItem(e.target);
                    if (item && item.querySelector('.item-options')) {
                        self._dragOp = new SlideDrag({
                            el: self.el,
                            item: item,
                            canSwipe: self.canSwipe
                        });
                        self._dragOp.start(e);
                        e.preventDefault();
                        self.isScrollFreeze = self.scrollView.freeze(true);
                    }
                }

                // If we had a last drag operation and this is a new one on a different item, clean that last one
                if (lastDragOp && self._dragOp && !self._dragOp.isSameItem(lastDragOp) && e.defaultPrevented) {
                    lastDragOp.clean && lastDragOp.clean();
                }
            },


            _handleEndDrag: function (e) {
                var self = this;

                if (self.scrollView) {
                    self.isScrollFreeze = self.scrollView.freeze(false);
                }

                self._didDragUpOrDown = false;

                if (!self._dragOp) {
                    return;
                }

                self._dragOp.end(e, function () {
                    self._initDrag();
                });
            },

            /**
             * Process the drag event to move the item to the left or right.
             */
            _handleDrag: function (e) {
                var self = this;

                if (Math.abs(e.gesture.deltaY) > 5) {
                    self._didDragUpOrDown = true;
                }

                // If we get a drag event, make sure we aren't in another drag, then check if we should
                // start one
                if (!self.isDragging && !self._dragOp) {
                    self._startDrag(e);
                }

                // No drag still, pass it up
                if (!self._dragOp) {
                    return;
                }

                e.gesture.srcEvent.preventDefault();
                self._dragOp.drag(e);
            }

        });

    })(ionic);

    (function (ionic) {
        'use strict';

        ionic.views.Modal = ionic.views.View.inherit({
            initialize: function (opts) {
                opts = ionic.extend({
                    focusFirstInput: false,
                    unfocusOnHide: true,
                    focusFirstDelay: 600,
                    backdropClickToClose: true,
                    hardwareBackButtonClose: true,
                }, opts);

                ionic.extend(this, opts);

                this.el = opts.el;
            },
            show: function () {
                var self = this;

                if (self.focusFirstInput) {
                    // Let any animations run first
                    window.setTimeout(function () {
                        var input = self.el.querySelector('input, textarea');
                        input && input.focus && input.focus();
                    }, self.focusFirstDelay);
                }
            },
            hide: function () {
                // Unfocus all elements
                if (this.unfocusOnHide) {
                    var inputs = this.el.querySelectorAll('input, textarea');
                    // Let any animations run first
                    window.setTimeout(function () {
                        for (var i = 0; i < inputs.length; i++) {
                            inputs[i].blur && inputs[i].blur();
                        }
                    });
                }
            }
        });

    })(ionic);

    (function (ionic) {
        'use strict';

        /**
         * The side menu view handles one of the side menu's in a Side Menu Controller
         * configuration.
         * It takes a DOM reference to that side menu element.
         */
        ionic.views.SideMenu = ionic.views.View.inherit({
            initialize: function (opts) {
                this.el = opts.el;
                this.isEnabled = (typeof opts.isEnabled === 'undefined') ? true : opts.isEnabled;
                this.setWidth(opts.width);
            },
            getFullWidth: function () {
                return this.width;
            },
            setWidth: function (width) {
                this.width = width;
                this.el.style.width = width + 'px';
            },
            setIsEnabled: function (isEnabled) {
                this.isEnabled = isEnabled;
            },
            bringUp: function () {
                if (this.el.style.zIndex !== '0') {
                    this.el.style.zIndex = '0';
                }
            },
            pushDown: function () {
                if (this.el.style.zIndex !== '-1') {
                    this.el.style.zIndex = '-1';
                }
            }
        });

        ionic.views.SideMenuContent = ionic.views.View.inherit({
            initialize: function (opts) {
                ionic.extend(this, {
                    animationClass: 'menu-animated',
                    onDrag: function () {
                    },
                    onEndDrag: function () {
                    }
                }, opts);

                ionic.onGesture('drag', ionic.proxy(this._onDrag, this), this.el);
                ionic.onGesture('release', ionic.proxy(this._onEndDrag, this), this.el);
            },
            _onDrag: function (e) {
                this.onDrag && this.onDrag(e);
            },
            _onEndDrag: function (e) {
                this.onEndDrag && this.onEndDrag(e);
            },
            disableAnimation: function () {
                this.el.classList.remove(this.animationClass);
            },
            enableAnimation: function () {
                this.el.classList.add(this.animationClass);
            },
            getTranslateX: function () {
                return parseFloat(this.el.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]);
            },
            setTranslateX: ionic.animationFrameThrottle(function (x) {
                this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + x + 'px, 0, 0)';
            })
        });

    })(ionic);

    /*
     * Adapted from Swipe.js 2.0
     *
     * Brad Birdsall
     * Copyright 2013, MIT License
     *
     */

    (function (ionic) {
        'use strict';

        ionic.views.Slider = ionic.views.View.inherit({
            initialize: function (options) {
                var slider = this;

                var touchStartEvent, touchMoveEvent, touchEndEvent;
                if (window.navigator.pointerEnabled) {
                    touchStartEvent = 'pointerdown';
                    touchMoveEvent = 'pointermove';
                    touchEndEvent = 'pointerup';
                } else if (window.navigator.msPointerEnabled) {
                    touchStartEvent = 'MSPointerDown';
                    touchMoveEvent = 'MSPointerMove';
                    touchEndEvent = 'MSPointerUp';
                } else {
                    touchStartEvent = 'touchstart';
                    touchMoveEvent = 'touchmove';
                    touchEndEvent = 'touchend';
                }

                var mouseStartEvent = 'mousedown';
                var mouseMoveEvent = 'mousemove';
                var mouseEndEvent = 'mouseup';

                // utilities
                var noop = function () {
                }; // simple no operation function
                var offloadFn = function (fn) {
                    setTimeout(fn || noop, 0);
                }; // offload a functions execution

                // check browser capabilities
                var browser = {
                    addEventListener: !!window.addEventListener,
                    transitions: (function (temp) {
                        var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
                        for (var i in props) if (temp.style[props[i]] !== undefined) return true;
                        return false;
                    })(document.createElement('swipe'))
                };


                var container = options.el;

                // quit if no root element
                if (!container) return;
                var element = container.children[0];
                var slides, slidePos, width, length;
                options = options || {};
                var index = parseInt(options.startSlide, 10) || 0;
                var speed = options.speed || 300;
                options.continuous = options.continuous !== undefined ? options.continuous : true;

                function setup() {

                    // do not setup if the container has no width
                    if (!container.offsetWidth) {
                        return;
                    }

                    // cache slides
                    slides = element.children;
                    length = slides.length;

                    // set continuous to false if only one slide
                    if (slides.length < 2) options.continuous = false;

                    //special case if two slides
                    if (browser.transitions && options.continuous && slides.length < 3) {
                        element.appendChild(slides[0].cloneNode(true));
                        element.appendChild(element.children[1].cloneNode(true));
                        slides = element.children;
                    }

                    // create an array to store current positions of each slide
                    slidePos = new Array(slides.length);

                    // determine width of each slide
                    width = container.offsetWidth || container.getBoundingClientRect().width;

                    element.style.width = (slides.length * width) + 'px';

                    // stack elements
                    var pos = slides.length;
                    while (pos--) {

                        var slide = slides[pos];

                        slide.style.width = width + 'px';
                        slide.setAttribute('data-index', pos);

                        if (browser.transitions) {
                            slide.style.left = (pos * -width) + 'px';
                            move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
                        }

                    }

                    // reposition elements before and after index
                    if (options.continuous && browser.transitions) {
                        move(circle(index - 1), -width, 0);
                        move(circle(index + 1), width, 0);
                    }

                    if (!browser.transitions) element.style.left = (index * -width) + 'px';

                    container.style.visibility = 'visible';

                    options.slidesChanged && options.slidesChanged();
                }

                function prev(slideSpeed) {

                    if (options.continuous) slide(index - 1, slideSpeed);
                    else if (index) slide(index - 1, slideSpeed);

                }

                function next(slideSpeed) {

                    if (options.continuous) slide(index + 1, slideSpeed);
                    else if (index < slides.length - 1) slide(index + 1, slideSpeed);

                }

                function circle(index) {

                    // a simple positive modulo using slides.length
                    return (slides.length + (index % slides.length)) % slides.length;

                }

                function slide(to, slideSpeed) {

                    // do nothing if already on requested slide
                    if (index == to) return;

                    if (!slides) {
                        index = to;
                        return;
                    }

                    if (browser.transitions) {

                        var direction = Math.abs(index - to) / (index - to); // 1: backward, -1: forward

                        // get the actual position of the slide
                        if (options.continuous) {
                            var naturalDirection = direction;
                            direction = -slidePos[circle(to)] / width;

                            // if going forward but to < index, use to = slides.length + to
                            // if going backward but to > index, use to = -slides.length + to
                            if (direction !== naturalDirection) to = -direction * slides.length + to;

                        }

                        var diff = Math.abs(index - to) - 1;

                        // move all the slides between index and to in the right direction
                        while (diff--) move(circle((to > index ? to : index) - diff - 1), width * direction, 0);

                        to = circle(to);

                        move(index, width * direction, slideSpeed || speed);
                        move(to, 0, slideSpeed || speed);

                        if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place

                    } else {

                        to = circle(to);
                        animate(index * -width, to * -width, slideSpeed || speed);
                        //no fallback for a circular continuous if the browser does not accept transitions
                    }

                    index = to;
                    offloadFn(options.callback && options.callback(index, slides[index]));
                }

                function move(index, dist, speed) {

                    translate(index, dist, speed);
                    slidePos[index] = dist;

                }

                function translate(index, dist, speed) {

                    var slide = slides[index];
                    var style = slide && slide.style;

                    if (!style) return;

                    style.webkitTransitionDuration =
                        style.MozTransitionDuration =
                            style.msTransitionDuration =
                                style.OTransitionDuration =
                                    style.transitionDuration = speed + 'ms';

                    style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
                    style.msTransform =
                        style.MozTransform =
                            style.OTransform = 'translateX(' + dist + 'px)';

                }

                function animate(from, to, speed) {

                    // if not an animation, just reposition
                    if (!speed) {

                        element.style.left = to + 'px';
                        return;

                    }

                    var start = +new Date();

                    var timer = setInterval(function () {

                        var timeElap = +new Date() - start;

                        if (timeElap > speed) {

                            element.style.left = to + 'px';

                            if (delay) begin();

                            options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);

                            clearInterval(timer);
                            return;

                        }

                        element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';

                    }, 4);

                }

                // setup auto slideshow
                var delay = options.auto || 0;
                var interval;

                function begin() {

                    interval = setTimeout(next, delay);

                }

                function stop() {

                    delay = options.auto || 0;
                    clearTimeout(interval);

                }


                // setup initial vars
                var start = {};
                var delta = {};
                var isScrolling;

                // setup event capturing
                var events = {

                    handleEvent: function (event) {
                        if (!event.touches && event.pageX && event.pageY) {
                            event.touches = [{
                                pageX: event.pageX,
                                pageY: event.pageY
                            }];
                        }

                        switch (event.type) {
                            case touchStartEvent:
                                this.start(event);
                                break;
                            case mouseStartEvent:
                                this.start(event);
                                break;
                            case touchMoveEvent:
                                this.touchmove(event);
                                break;
                            case mouseMoveEvent:
                                this.touchmove(event);
                                break;
                            case touchEndEvent:
                                offloadFn(this.end(event));
                                break;
                            case mouseEndEvent:
                                offloadFn(this.end(event));
                                break;
                            case 'webkitTransitionEnd':
                            case 'msTransitionEnd':
                            case 'oTransitionEnd':
                            case 'otransitionend':
                            case 'transitionend':
                                offloadFn(this.transitionEnd(event));
                                break;
                            case 'resize':
                                offloadFn(setup);
                                break;
                        }

                        if (options.stopPropagation) event.stopPropagation();

                    },
                    start: function (event) {

                        // prevent to start if there is no valid event
                        if (!event.touches) {
                            return;
                        }

                        var touches = event.touches[0];

                        // measure start values
                        start = {

                            // get initial touch coords
                            x: touches.pageX,
                            y: touches.pageY,

                            // store time to determine touch duration
                            time: +new Date()

                        };

                        // used for testing first move event
                        isScrolling = undefined;

                        // reset delta and end measurements
                        delta = {};

                        // attach touchmove and touchend listeners
                        element.addEventListener(touchMoveEvent, this, false);
                        element.addEventListener(mouseMoveEvent, this, false);

                        element.addEventListener(touchEndEvent, this, false);
                        element.addEventListener(mouseEndEvent, this, false);

                        document.addEventListener(touchEndEvent, this, false);
                        document.addEventListener(mouseEndEvent, this, false);
                    },
                    touchmove: function (event) {

                        // ensure there is a valid event
                        // ensure swiping with one touch and not pinching
                        // ensure sliding is enabled
                        if (!event.touches ||
                            event.touches.length > 1 ||
                            event.scale && event.scale !== 1 ||
                            slider.slideIsDisabled) {
                            return;
                        }

                        if (options.disableScroll) event.preventDefault();

                        var touches = event.touches[0];

                        // measure change in x and y
                        delta = {
                            x: touches.pageX - start.x,
                            y: touches.pageY - start.y
                        };

                        // determine if scrolling test has run - one time test
                        if (typeof isScrolling == 'undefined') {
                            isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
                        }

                        // if user is not trying to scroll vertically
                        if (!isScrolling) {

                            // prevent native scrolling
                            event.preventDefault();

                            // stop slideshow
                            stop();

                            // increase resistance if first or last slide
                            if (options.continuous) { // we don't add resistance at the end

                                translate(circle(index - 1), delta.x + slidePos[circle(index - 1)], 0);
                                translate(index, delta.x + slidePos[index], 0);
                                translate(circle(index + 1), delta.x + slidePos[circle(index + 1)], 0);

                            } else {
                                // If the slider bounces, do the bounce!
                                if (options.bouncing) {
                                    delta.x =
                                        delta.x /
                                        ( (!index && delta.x > 0 ||         // if first slide and sliding left
                                            index == slides.length - 1 &&     // or if last slide and sliding right
                                            delta.x < 0                       // and if sliding at all
                                        ) ?
                                            ( Math.abs(delta.x) / width + 1 )      // determine resistance level
                                            : 1 );                                 // no resistance if false
                                } else {
                                    if (width * index - delta.x < 0) {               //We are trying scroll past left boundary
                                        delta.x = Math.min(delta.x, width * index);  //Set delta.x so we don't go past left screen
                                    }
                                    if (Math.abs(delta.x) > width * (slides.length - index - 1)) {         //We are trying to scroll past right bondary
                                        delta.x = Math.max(-width * (slides.length - index - 1), delta.x);  //Set delta.x so we don't go past right screen
                                    }
                                }

                                // translate 1:1
                                translate(index - 1, delta.x + slidePos[index - 1], 0);
                                translate(index, delta.x + slidePos[index], 0);
                                translate(index + 1, delta.x + slidePos[index + 1], 0);
                            }

                            options.onDrag && options.onDrag();
                        }

                    },
                    end: function () {

                        // measure duration
                        var duration = +new Date() - start.time;

                        // determine if slide attempt triggers next/prev slide
                        var isValidSlide =
                            Number(duration) < 250 &&         // if slide duration is less than 250ms
                            Math.abs(delta.x) > 20 ||         // and if slide amt is greater than 20px
                            Math.abs(delta.x) > width / 2;      // or if slide amt is greater than half the width

                        // determine if slide attempt is past start and end
                        var isPastBounds = (!index && delta.x > 0) ||      // if first slide and slide amt is greater than 0
                            (index == slides.length - 1 && delta.x < 0); // or if last slide and slide amt is less than 0

                        if (options.continuous) isPastBounds = false;

                        // determine direction of swipe (true:right, false:left)
                        var direction = delta.x < 0;

                        // if not scrolling vertically
                        if (!isScrolling) {

                            if (isValidSlide && !isPastBounds) {

                                if (direction) {

                                    if (options.continuous) { // we need to get the next in this direction in place

                                        move(circle(index - 1), -width, 0);
                                        move(circle(index + 2), width, 0);

                                    } else {
                                        move(index - 1, -width, 0);
                                    }

                                    move(index, slidePos[index] - width, speed);
                                    move(circle(index + 1), slidePos[circle(index + 1)] - width, speed);
                                    index = circle(index + 1);

                                } else {
                                    if (options.continuous) { // we need to get the next in this direction in place

                                        move(circle(index + 1), width, 0);
                                        move(circle(index - 2), -width, 0);

                                    } else {
                                        move(index + 1, width, 0);
                                    }

                                    move(index, slidePos[index] + width, speed);
                                    move(circle(index - 1), slidePos[circle(index - 1)] + width, speed);
                                    index = circle(index - 1);

                                }

                                options.callback && options.callback(index, slides[index]);

                            } else {

                                if (options.continuous) {

                                    move(circle(index - 1), -width, speed);
                                    move(index, 0, speed);
                                    move(circle(index + 1), width, speed);

                                } else {

                                    move(index - 1, -width, speed);
                                    move(index, 0, speed);
                                    move(index + 1, width, speed);
                                }

                            }

                        }

                        // kill touchmove and touchend event listeners until touchstart called again
                        element.removeEventListener(touchMoveEvent, events, false);
                        element.removeEventListener(mouseMoveEvent, events, false);

                        element.removeEventListener(touchEndEvent, events, false);
                        element.removeEventListener(mouseEndEvent, events, false);

                        document.removeEventListener(touchEndEvent, events, false);
                        document.removeEventListener(mouseEndEvent, events, false);

                        options.onDragEnd && options.onDragEnd();
                    },
                    transitionEnd: function (event) {

                        if (parseInt(event.target.getAttribute('data-index'), 10) == index) {

                            if (delay) begin();

                            options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);

                        }

                    }

                };

                // Public API
                this.update = function () {
                    setTimeout(setup);
                };
                this.setup = function () {
                    setup();
                };

                this.loop = function (value) {
                    if (arguments.length) options.continuous = !!value;
                    return options.continuous;
                };

                this.enableSlide = function (shouldEnable) {
                    if (arguments.length) {
                        this.slideIsDisabled = !shouldEnable;
                    }
                    return !this.slideIsDisabled;
                };

                this.slide = this.select = function (to, speed) {
                    // cancel slideshow
                    stop();

                    slide(to, speed);
                };

                this.prev = this.previous = function () {
                    // cancel slideshow
                    stop();

                    prev();
                };

                this.next = function () {
                    // cancel slideshow
                    stop();

                    next();
                };

                this.stop = function () {
                    // cancel slideshow
                    stop();
                };

                this.start = function () {
                    begin();
                };

                this.autoPlay = function (newDelay) {
                    if (!delay || delay < 0) {
                        stop();
                    } else {
                        delay = newDelay;
                        begin();
                    }
                };

                this.currentIndex = this.selected = function () {
                    // return current index position
                    return index;
                };

                this.slidesCount = this.count = function () {
                    // return total number of slides
                    return length;
                };

                this.kill = function () {
                    // cancel slideshow
                    stop();

                    // reset element
                    element.style.width = '';
                    element.style.left = '';

                    // reset slides so no refs are held on to
                    slides && (slides = []);

                    // removed event listeners
                    if (browser.addEventListener) {

                        // remove current event listeners
                        element.removeEventListener(touchStartEvent, events, false);
                        element.removeEventListener(mouseStartEvent, events, false);
                        element.removeEventListener('webkitTransitionEnd', events, false);
                        element.removeEventListener('msTransitionEnd', events, false);
                        element.removeEventListener('oTransitionEnd', events, false);
                        element.removeEventListener('otransitionend', events, false);
                        element.removeEventListener('transitionend', events, false);
                        window.removeEventListener('resize', events, false);

                    }
                    else {

                        window.onresize = null;

                    }
                };

                this.load = function () {
                    // trigger setup
                    setup();

                    // start auto slideshow if applicable
                    if (delay) begin();


                    // add event listeners
                    if (browser.addEventListener) {

                        // set touchstart event on element
                        element.addEventListener(touchStartEvent, events, false);
                        element.addEventListener(mouseStartEvent, events, false);

                        if (browser.transitions) {
                            element.addEventListener('webkitTransitionEnd', events, false);
                            element.addEventListener('msTransitionEnd', events, false);
                            element.addEventListener('oTransitionEnd', events, false);
                            element.addEventListener('otransitionend', events, false);
                            element.addEventListener('transitionend', events, false);
                        }

                        // set resize event on window
                        window.addEventListener('resize', events, false);

                    } else {

                        window.onresize = function () {
                            setup();
                        }; // to play nice with old IE

                    }
                };

            }
        });

    })(ionic);

    /*eslint space-after-keywords: 0*/

    /**
     * Swiper 3.2.7
     * Most modern mobile touch slider and framework with hardware accelerated transitions
     *
     * http://www.idangero.us/swiper/
     *
     * Copyright 2015, Vladimir Kharlampidi
     * The iDangero.us
     * http://www.idangero.us/
     *
     * Licensed under MIT
     *
     * Released on: December 7, 2015
     */
    (function () {
        'use strict';
        var $;
        /*===========================
         Swiper
         ===========================*/
        var Swiper = function (container, params, _scope, $compile) {

            if (!(this instanceof Swiper)) return new Swiper(container, params);

            var defaults = {
                direction: 'horizontal',
                touchEventsTarget: 'container',
                initialSlide: 0,
                speed: 300,
                // autoplay
                autoplay: false,
                autoplayDisableOnInteraction: true,
                // To support iOS's swipe-to-go-back gesture (when being used in-app, with UIWebView).
                iOSEdgeSwipeDetection: false,
                iOSEdgeSwipeThreshold: 20,
                // Free mode
                freeMode: false,
                freeModeMomentum: true,
                freeModeMomentumRatio: 1,
                freeModeMomentumBounce: true,
                freeModeMomentumBounceRatio: 1,
                freeModeSticky: false,
                freeModeMinimumVelocity: 0.02,
                // Autoheight
                autoHeight: false,
                // Set wrapper width
                setWrapperSize: false,
                // Virtual Translate
                virtualTranslate: false,
                // Effects
                effect: 'slide', // 'slide' or 'fade' or 'cube' or 'coverflow'
                coverflow: {
                    rotate: 50,
                    stretch: 0,
                    depth: 100,
                    modifier: 1,
                    slideShadows: true
                },
                cube: {
                    slideShadows: true,
                    shadow: true,
                    shadowOffset: 20,
                    shadowScale: 0.94
                },
                fade: {
                    crossFade: false
                },
                // Parallax
                parallax: false,
                // Scrollbar
                scrollbar: null,
                scrollbarHide: true,
                scrollbarDraggable: false,
                scrollbarSnapOnRelease: false,
                // Keyboard Mousewheel
                keyboardControl: false,
                mousewheelControl: false,
                mousewheelReleaseOnEdges: false,
                mousewheelInvert: false,
                mousewheelForceToAxis: false,
                mousewheelSensitivity: 1,
                // Hash Navigation
                hashnav: false,
                // Breakpoints
                breakpoints: undefined,
                // Slides grid
                spaceBetween: 0,
                slidesPerView: 1,
                slidesPerColumn: 1,
                slidesPerColumnFill: 'column',
                slidesPerGroup: 1,
                centeredSlides: false,
                slidesOffsetBefore: 0, // in px
                slidesOffsetAfter: 0, // in px
                // Round length
                roundLengths: false,
                // Touches
                touchRatio: 1,
                touchAngle: 45,
                simulateTouch: true,
                shortSwipes: true,
                longSwipes: true,
                longSwipesRatio: 0.5,
                longSwipesMs: 300,
                followFinger: true,
                onlyExternal: false,
                threshold: 0,
                touchMoveStopPropagation: true,
                // Pagination
                pagination: null,
                paginationElement: 'span',
                paginationClickable: false,
                paginationHide: false,
                paginationBulletRender: null,
                // Resistance
                resistance: true,
                resistanceRatio: 0.85,
                // Next/prev buttons
                nextButton: null,
                prevButton: null,
                // Progress
                watchSlidesProgress: false,
                watchSlidesVisibility: false,
                // Cursor
                grabCursor: false,
                // Clicks
                preventClicks: true,
                preventClicksPropagation: true,
                slideToClickedSlide: false,
                // Lazy Loading
                lazyLoading: false,
                lazyLoadingInPrevNext: false,
                lazyLoadingOnTransitionStart: false,
                // Images
                preloadImages: true,
                updateOnImagesReady: true,
                // loop
                loop: false,
                loopAdditionalSlides: 0,
                loopedSlides: null,
                // Control
                control: undefined,
                controlInverse: false,
                controlBy: 'slide', //or 'container'
                // Swiping/no swiping
                allowSwipeToPrev: true,
                allowSwipeToNext: true,
                swipeHandler: null, //'.swipe-handler',
                noSwiping: true,
                noSwipingClass: 'swiper-no-swiping',
                // NS
                slideClass: 'swiper-slide',
                slideActiveClass: 'swiper-slide-active',
                slideVisibleClass: 'swiper-slide-visible',
                slideDuplicateClass: 'swiper-slide-duplicate',
                slideNextClass: 'swiper-slide-next',
                slidePrevClass: 'swiper-slide-prev',
                wrapperClass: 'swiper-wrapper',
                bulletClass: 'swiper-pagination-bullet',
                bulletActiveClass: 'swiper-pagination-bullet-active',
                buttonDisabledClass: 'swiper-button-disabled',
                paginationHiddenClass: 'swiper-pagination-hidden',
                // Observer
                observer: false,
                observeParents: false,
                // Accessibility
                a11y: false,
                prevSlideMessage: 'Previous slide',
                nextSlideMessage: 'Next slide',
                firstSlideMessage: 'This is the first slide',
                lastSlideMessage: 'This is the last slide',
                paginationBulletMessage: 'Go to slide {{index}}',
                // Callbacks
                runCallbacksOnInit: true
                /*
                 Callbacks:
                 onInit: function (swiper)
                 onDestroy: function (swiper)
                 onClick: function (swiper, e)
                 onTap: function (swiper, e)
                 onDoubleTap: function (swiper, e)
                 onSliderMove: function (swiper, e)
                 onSlideChangeStart: function (swiper)
                 onSlideChangeEnd: function (swiper)
                 onTransitionStart: function (swiper)
                 onTransitionEnd: function (swiper)
                 onImagesReady: function (swiper)
                 onProgress: function (swiper, progress)
                 onTouchStart: function (swiper, e)
                 onTouchMove: function (swiper, e)
                 onTouchMoveOpposite: function (swiper, e)
                 onTouchEnd: function (swiper, e)
                 onReachBeginning: function (swiper)
                 onReachEnd: function (swiper)
                 onSetTransition: function (swiper, duration)
                 onSetTranslate: function (swiper, translate)
                 onAutoplayStart: function (swiper)
                 onAutoplayStop: function (swiper),
                 onLazyImageLoad: function (swiper, slide, image)
                 onLazyImageReady: function (swiper, slide, image)
                 */

            };
            var initialVirtualTranslate = params && params.virtualTranslate;

            params = params || {};
            var originalParams = {};
            for (var param in params) {
                if (typeof params[param] === 'object' && !(params[param].nodeType || params[param] === window || params[param] === document || (typeof Dom7 !== 'undefined' && params[param] instanceof Dom7) || (typeof jQuery !== 'undefined' && params[param] instanceof jQuery))) {
                    originalParams[param] = {};
                    for (var deepParam in params[param]) {
                        originalParams[param][deepParam] = params[param][deepParam];
                    }
                }
                else {
                    originalParams[param] = params[param];
                }
            }
            for (var def in defaults) {
                if (typeof params[def] === 'undefined') {
                    params[def] = defaults[def];
                }
                else if (typeof params[def] === 'object') {
                    for (var deepDef in defaults[def]) {
                        if (typeof params[def][deepDef] === 'undefined') {
                            params[def][deepDef] = defaults[def][deepDef];
                        }
                    }
                }
            }

            // Swiper
            var s = this;

            // Params
            s.params = params;
            s.originalParams = originalParams;

            // Classname
            s.classNames = [];
            /*=========================
             Dom Library and plugins
             ===========================*/
            if (typeof $ !== 'undefined' && typeof Dom7 !== 'undefined') {
                $ = Dom7;
            }
            if (typeof $ === 'undefined') {
                if (typeof Dom7 === 'undefined') {
                    $ = window.Dom7 || window.Zepto || window.jQuery;
                }
                else {
                    $ = Dom7;
                }
                if (!$) return;
            }
            // Export it to Swiper instance
            s.$ = $;

            /*=========================
             Breakpoints
             ===========================*/
            s.currentBreakpoint = undefined;
            s.getActiveBreakpoint = function () {
                //Get breakpoint for window width
                if (!s.params.breakpoints) return false;
                var breakpoint = false;
                var points = [], point;
                for (point in s.params.breakpoints) {
                    if (s.params.breakpoints.hasOwnProperty(point)) {
                        points.push(point);
                    }
                }
                points.sort(function (a, b) {
                    return parseInt(a, 10) > parseInt(b, 10);
                });
                for (var i = 0; i < points.length; i++) {
                    point = points[i];
                    if (point >= window.innerWidth && !breakpoint) {
                        breakpoint = point;
                    }
                }
                return breakpoint || 'max';
            };
            s.setBreakpoint = function () {
                //Set breakpoint for window width and update parameters
                var breakpoint = s.getActiveBreakpoint();
                if (breakpoint && s.currentBreakpoint !== breakpoint) {
                    var breakPointsParams = breakpoint in s.params.breakpoints ? s.params.breakpoints[breakpoint] : s.originalParams;
                    for (var param in breakPointsParams) {
                        s.params[param] = breakPointsParams[param];
                    }
                    s.currentBreakpoint = breakpoint;
                }
            };
            // Set breakpoint on load
            if (s.params.breakpoints) {
                s.setBreakpoint();
            }

            /*=========================
             Preparation - Define Container, Wrapper and Pagination
             ===========================*/
            s.container = $(container);
            if (s.container.length === 0) return;
            if (s.container.length > 1) {
                s.container.each(function () {
                    new Swiper(this, params);
                });
                return;
            }

            // Save instance in container HTML Element and in data
            s.container[0].swiper = s;
            s.container.data('swiper', s);

            s.classNames.push('swiper-container-' + s.params.direction);

            if (s.params.freeMode) {
                s.classNames.push('swiper-container-free-mode');
            }
            if (!s.support.flexbox) {
                s.classNames.push('swiper-container-no-flexbox');
                s.params.slidesPerColumn = 1;
            }
            if (s.params.autoHeight) {
                s.classNames.push('swiper-container-autoheight');
            }
            // Enable slides progress when required
            if (s.params.parallax || s.params.watchSlidesVisibility) {
                s.params.watchSlidesProgress = true;
            }
            // Coverflow / 3D
            if (['cube', 'coverflow'].indexOf(s.params.effect) >= 0) {
                if (s.support.transforms3d) {
                    s.params.watchSlidesProgress = true;
                    s.classNames.push('swiper-container-3d');
                }
                else {
                    s.params.effect = 'slide';
                }
            }
            if (s.params.effect !== 'slide') {
                s.classNames.push('swiper-container-' + s.params.effect);
            }
            if (s.params.effect === 'cube') {
                s.params.resistanceRatio = 0;
                s.params.slidesPerView = 1;
                s.params.slidesPerColumn = 1;
                s.params.slidesPerGroup = 1;
                s.params.centeredSlides = false;
                s.params.spaceBetween = 0;
                s.params.virtualTranslate = true;
                s.params.setWrapperSize = false;
            }
            if (s.params.effect === 'fade') {
                s.params.slidesPerView = 1;
                s.params.slidesPerColumn = 1;
                s.params.slidesPerGroup = 1;
                s.params.watchSlidesProgress = true;
                s.params.spaceBetween = 0;
                if (typeof initialVirtualTranslate === 'undefined') {
                    s.params.virtualTranslate = true;
                }
            }

            // Grab Cursor
            if (s.params.grabCursor && s.support.touch) {
                s.params.grabCursor = false;
            }

            // Wrapper
            s.wrapper = s.container.children('.' + s.params.wrapperClass);

            // Pagination
            if (s.params.pagination) {
                s.paginationContainer = $(s.params.pagination);
                if (s.params.paginationClickable) {
                    s.paginationContainer.addClass('swiper-pagination-clickable');
                }
            }

            // Is Horizontal
            function isH() {
                return s.params.direction === 'horizontal';
            }

            // RTL
            s.rtl = isH() && (s.container[0].dir.toLowerCase() === 'rtl' || s.container.css('direction') === 'rtl');
            if (s.rtl) {
                s.classNames.push('swiper-container-rtl');
            }

            // Wrong RTL support
            if (s.rtl) {
                s.wrongRTL = s.wrapper.css('display') === '-webkit-box';
            }

            // Columns
            if (s.params.slidesPerColumn > 1) {
                s.classNames.push('swiper-container-multirow');
            }

            // Check for Android
            if (s.device.android) {
                s.classNames.push('swiper-container-android');
            }

            // Add classes
            s.container.addClass(s.classNames.join(' '));

            // Translate
            s.translate = 0;

            // Progress
            s.progress = 0;

            // Velocity
            s.velocity = 0;

            /*=========================
             Locks, unlocks
             ===========================*/
            s.lockSwipeToNext = function () {
                s.params.allowSwipeToNext = false;
            };
            s.lockSwipeToPrev = function () {
                s.params.allowSwipeToPrev = false;
            };
            s.lockSwipes = function () {
                s.params.allowSwipeToNext = s.params.allowSwipeToPrev = false;
            };
            s.unlockSwipeToNext = function () {
                s.params.allowSwipeToNext = true;
            };
            s.unlockSwipeToPrev = function () {
                s.params.allowSwipeToPrev = true;
            };
            s.unlockSwipes = function () {
                s.params.allowSwipeToNext = s.params.allowSwipeToPrev = true;
            };

            /*=========================
             Round helper
             ===========================*/
            function round(a) {
                return Math.floor(a);
            }

            /*=========================
             Set grab cursor
             ===========================*/
            if (s.params.grabCursor) {
                s.container[0].style.cursor = 'move';
                s.container[0].style.cursor = '-webkit-grab';
                s.container[0].style.cursor = '-moz-grab';
                s.container[0].style.cursor = 'grab';
            }
            /*=========================
             Update on Images Ready
             ===========================*/
            s.imagesToLoad = [];
            s.imagesLoaded = 0;

            s.loadImage = function (imgElement, src, srcset, checkForComplete, callback) {
                var image;

                function onReady() {
                    if (callback) callback();
                }

                if (!imgElement.complete || !checkForComplete) {
                    if (src) {
                        image = new window.Image();
                        image.onload = onReady;
                        image.onerror = onReady;
                        if (srcset) {
                            image.srcset = srcset;
                        }
                        if (src) {
                            image.src = src;
                        }
                    } else {
                        onReady();
                    }

                } else {//image already loaded...
                    onReady();
                }
            };
            s.preloadImages = function () {
                s.imagesToLoad = s.container.find('img');
                function _onReady() {
                    if (typeof s === 'undefined' || s === null) return;
                    if (s.imagesLoaded !== undefined) s.imagesLoaded++;
                    if (s.imagesLoaded === s.imagesToLoad.length) {
                        if (s.params.updateOnImagesReady) s.update();
                        s.emit('onImagesReady', s);
                    }
                }

                for (var i = 0; i < s.imagesToLoad.length; i++) {
                    s.loadImage(s.imagesToLoad[i], (s.imagesToLoad[i].currentSrc || s.imagesToLoad[i].getAttribute('src')), (s.imagesToLoad[i].srcset || s.imagesToLoad[i].getAttribute('srcset')), true, _onReady);
                }
            };

            /*=========================
             Autoplay
             ===========================*/
            s.autoplayTimeoutId = undefined;
            s.autoplaying = false;
            s.autoplayPaused = false;
            function autoplay() {
                s.autoplayTimeoutId = setTimeout(function () {
                    if (s.params.loop) {
                        s.fixLoop();
                        s._slideNext();
                    }
                    else {
                        if (!s.isEnd) {
                            s._slideNext();
                        }
                        else {
                            if (!params.autoplayStopOnLast) {
                                s._slideTo(0);
                            }
                            else {
                                s.stopAutoplay();
                            }
                        }
                    }
                }, s.params.autoplay);
            }

            s.startAutoplay = function () {
                if (typeof s.autoplayTimeoutId !== 'undefined') return false;
                if (!s.params.autoplay) return false;
                if (s.autoplaying) return false;
                s.autoplaying = true;
                s.emit('onAutoplayStart', s);
                autoplay();
            };
            s.stopAutoplay = function (internal) {
                if (!s.autoplayTimeoutId) return;
                if (s.autoplayTimeoutId) clearTimeout(s.autoplayTimeoutId);
                s.autoplaying = false;
                s.autoplayTimeoutId = undefined;
                s.emit('onAutoplayStop', s);
            };
            s.pauseAutoplay = function (speed) {
                if (s.autoplayPaused) return;
                if (s.autoplayTimeoutId) clearTimeout(s.autoplayTimeoutId);
                s.autoplayPaused = true;
                if (speed === 0) {
                    s.autoplayPaused = false;
                    autoplay();
                }
                else {
                    s.wrapper.transitionEnd(function () {
                        if (!s) return;
                        s.autoplayPaused = false;
                        if (!s.autoplaying) {
                            s.stopAutoplay();
                        }
                        else {
                            autoplay();
                        }
                    });
                }
            };
            /*=========================
             Min/Max Translate
             ===========================*/
            s.minTranslate = function () {
                return (-s.snapGrid[0]);
            };
            s.maxTranslate = function () {
                return (-s.snapGrid[s.snapGrid.length - 1]);
            };
            /*=========================
             Slider/slides sizes
             ===========================*/
            s.updateAutoHeight = function () {
                // Update Height
                var newHeight = s.slides.eq(s.activeIndex)[0].offsetHeight;
                if (newHeight) s.wrapper.css('height', s.slides.eq(s.activeIndex)[0].offsetHeight + 'px');
            };
            s.updateContainerSize = function () {
                var width, height;
                if (typeof s.params.width !== 'undefined') {
                    width = s.params.width;
                }
                else {
                    width = s.container[0].clientWidth;
                }
                if (typeof s.params.height !== 'undefined') {
                    height = s.params.height;
                }
                else {
                    height = s.container[0].clientHeight;
                }
                if (width === 0 && isH() || height === 0 && !isH()) {
                    return;
                }

                //Subtract paddings
                width = width - parseInt(s.container.css('padding-left'), 10) - parseInt(s.container.css('padding-right'), 10);
                height = height - parseInt(s.container.css('padding-top'), 10) - parseInt(s.container.css('padding-bottom'), 10);

                // Store values
                s.width = width;
                s.height = height;
                s.size = isH() ? s.width : s.height;
            };

            s.updateSlidesSize = function () {
                s.slides = s.wrapper.children('.' + s.params.slideClass);
                s.snapGrid = [];
                s.slidesGrid = [];
                s.slidesSizesGrid = [];

                var spaceBetween = s.params.spaceBetween,
                    slidePosition = -s.params.slidesOffsetBefore,
                    i,
                    prevSlideSize = 0,
                    index = 0;
                if (typeof spaceBetween === 'string' && spaceBetween.indexOf('%') >= 0) {
                    spaceBetween = parseFloat(spaceBetween.replace('%', '')) / 100 * s.size;
                }

                s.virtualSize = -spaceBetween;
                // reset margins
                if (s.rtl) s.slides.css({marginLeft: '', marginTop: ''});
                else s.slides.css({marginRight: '', marginBottom: ''});

                var slidesNumberEvenToRows;
                if (s.params.slidesPerColumn > 1) {
                    if (Math.floor(s.slides.length / s.params.slidesPerColumn) === s.slides.length / s.params.slidesPerColumn) {
                        slidesNumberEvenToRows = s.slides.length;
                    }
                    else {
                        slidesNumberEvenToRows = Math.ceil(s.slides.length / s.params.slidesPerColumn) * s.params.slidesPerColumn;
                    }
                    if (s.params.slidesPerView !== 'auto' && s.params.slidesPerColumnFill === 'row') {
                        slidesNumberEvenToRows = Math.max(slidesNumberEvenToRows, s.params.slidesPerView * s.params.slidesPerColumn);
                    }
                }

                // Calc slides
                var slideSize;
                var slidesPerColumn = s.params.slidesPerColumn;
                var slidesPerRow = slidesNumberEvenToRows / slidesPerColumn;
                var numFullColumns = slidesPerRow - (s.params.slidesPerColumn * slidesPerRow - s.slides.length);
                for (i = 0; i < s.slides.length; i++) {
                    slideSize = 0;
                    var slide = s.slides.eq(i);
                    if (s.params.slidesPerColumn > 1) {
                        // Set slides order
                        var newSlideOrderIndex;
                        var column, row;
                        if (s.params.slidesPerColumnFill === 'column') {
                            column = Math.floor(i / slidesPerColumn);
                            row = i - column * slidesPerColumn;
                            if (column > numFullColumns || (column === numFullColumns && row === slidesPerColumn - 1)) {
                                if (++row >= slidesPerColumn) {
                                    row = 0;
                                    column++;
                                }
                            }
                            newSlideOrderIndex = column + row * slidesNumberEvenToRows / slidesPerColumn;
                            slide
                                .css({
                                    '-webkit-box-ordinal-group': newSlideOrderIndex,
                                    '-moz-box-ordinal-group': newSlideOrderIndex,
                                    '-ms-flex-order': newSlideOrderIndex,
                                    '-webkit-order': newSlideOrderIndex,
                                    'order': newSlideOrderIndex
                                });
                        }
                        else {
                            row = Math.floor(i / slidesPerRow);
                            column = i - row * slidesPerRow;
                        }
                        slide
                            .css({
                                'margin-top': (row !== 0 && s.params.spaceBetween) && (s.params.spaceBetween + 'px')
                            })
                            .attr('data-swiper-column', column)
                            .attr('data-swiper-row', row);

                    }
                    if (slide.css('display') === 'none') continue;
                    if (s.params.slidesPerView === 'auto') {
                        slideSize = isH() ? slide.outerWidth(true) : slide.outerHeight(true);
                        if (s.params.roundLengths) slideSize = round(slideSize);
                    }
                    else {
                        slideSize = (s.size - (s.params.slidesPerView - 1) * spaceBetween) / s.params.slidesPerView;
                        if (s.params.roundLengths) slideSize = round(slideSize);

                        if (isH()) {
                            s.slides[i].style.width = slideSize + 'px';
                        }
                        else {
                            s.slides[i].style.height = slideSize + 'px';
                        }
                    }
                    s.slides[i].swiperSlideSize = slideSize;
                    s.slidesSizesGrid.push(slideSize);


                    if (s.params.centeredSlides) {
                        slidePosition = slidePosition + slideSize / 2 + prevSlideSize / 2 + spaceBetween;
                        if (i === 0) slidePosition = slidePosition - s.size / 2 - spaceBetween;
                        if (Math.abs(slidePosition) < 1 / 1000) slidePosition = 0;
                        if ((index) % s.params.slidesPerGroup === 0) s.snapGrid.push(slidePosition);
                        s.slidesGrid.push(slidePosition);
                    }
                    else {
                        if ((index) % s.params.slidesPerGroup === 0) s.snapGrid.push(slidePosition);
                        s.slidesGrid.push(slidePosition);
                        slidePosition = slidePosition + slideSize + spaceBetween;
                    }

                    s.virtualSize += slideSize + spaceBetween;

                    prevSlideSize = slideSize;

                    index++;
                }
                s.virtualSize = Math.max(s.virtualSize, s.size) + s.params.slidesOffsetAfter;
                var newSlidesGrid;

                if (
                    s.rtl && s.wrongRTL && (s.params.effect === 'slide' || s.params.effect === 'coverflow')) {
                    s.wrapper.css({width: s.virtualSize + s.params.spaceBetween + 'px'});
                }
                if (!s.support.flexbox || s.params.setWrapperSize) {
                    if (isH()) s.wrapper.css({width: s.virtualSize + s.params.spaceBetween + 'px'});
                    else s.wrapper.css({height: s.virtualSize + s.params.spaceBetween + 'px'});
                }

                if (s.params.slidesPerColumn > 1) {
                    s.virtualSize = (slideSize + s.params.spaceBetween) * slidesNumberEvenToRows;
                    s.virtualSize = Math.ceil(s.virtualSize / s.params.slidesPerColumn) - s.params.spaceBetween;
                    s.wrapper.css({width: s.virtualSize + s.params.spaceBetween + 'px'});
                    if (s.params.centeredSlides) {
                        newSlidesGrid = [];
                        for (i = 0; i < s.snapGrid.length; i++) {
                            if (s.snapGrid[i] < s.virtualSize + s.snapGrid[0]) newSlidesGrid.push(s.snapGrid[i]);
                        }
                        s.snapGrid = newSlidesGrid;
                    }
                }

                // Remove last grid elements depending on width
                if (!s.params.centeredSlides) {
                    newSlidesGrid = [];
                    for (i = 0; i < s.snapGrid.length; i++) {
                        if (s.snapGrid[i] <= s.virtualSize - s.size) {
                            newSlidesGrid.push(s.snapGrid[i]);
                        }
                    }
                    s.snapGrid = newSlidesGrid;
                    if (Math.floor(s.virtualSize - s.size) > Math.floor(s.snapGrid[s.snapGrid.length - 1])) {
                        s.snapGrid.push(s.virtualSize - s.size);
                    }
                }
                if (s.snapGrid.length === 0) s.snapGrid = [0];

                if (s.params.spaceBetween !== 0) {
                    if (isH()) {
                        if (s.rtl) s.slides.css({marginLeft: spaceBetween + 'px'});
                        else s.slides.css({marginRight: spaceBetween + 'px'});
                    }
                    else s.slides.css({marginBottom: spaceBetween + 'px'});
                }
                if (s.params.watchSlidesProgress) {
                    s.updateSlidesOffset();
                }
            };
            s.updateSlidesOffset = function () {
                for (var i = 0; i < s.slides.length; i++) {
                    s.slides[i].swiperSlideOffset = isH() ? s.slides[i].offsetLeft : s.slides[i].offsetTop;
                }
            };

            /*=========================
             Slider/slides progress
             ===========================*/
            s.updateSlidesProgress = function (translate) {
                if (typeof translate === 'undefined') {
                    translate = s.translate || 0;
                }
                if (s.slides.length === 0) return;
                if (typeof s.slides[0].swiperSlideOffset === 'undefined') s.updateSlidesOffset();

                var offsetCenter = -translate;
                if (s.rtl) offsetCenter = translate;

                // Visible Slides
                s.slides.removeClass(s.params.slideVisibleClass);
                for (var i = 0; i < s.slides.length; i++) {
                    var slide = s.slides[i];
                    var slideProgress = (offsetCenter - slide.swiperSlideOffset) / (slide.swiperSlideSize + s.params.spaceBetween);
                    if (s.params.watchSlidesVisibility) {
                        var slideBefore = -(offsetCenter - slide.swiperSlideOffset);
                        var slideAfter = slideBefore + s.slidesSizesGrid[i];
                        var isVisible =
                            (slideBefore >= 0 && slideBefore < s.size) ||
                            (slideAfter > 0 && slideAfter <= s.size) ||
                            (slideBefore <= 0 && slideAfter >= s.size);
                        if (isVisible) {
                            s.slides.eq(i).addClass(s.params.slideVisibleClass);
                        }
                    }
                    slide.progress = s.rtl ? -slideProgress : slideProgress;
                }
            };
            s.updateProgress = function (translate) {
                if (typeof translate === 'undefined') {
                    translate = s.translate || 0;
                }
                var translatesDiff = s.maxTranslate() - s.minTranslate();
                var wasBeginning = s.isBeginning;
                var wasEnd = s.isEnd;
                if (translatesDiff === 0) {
                    s.progress = 0;
                    s.isBeginning = s.isEnd = true;
                }
                else {
                    s.progress = (translate - s.minTranslate()) / (translatesDiff);
                    s.isBeginning = s.progress <= 0;
                    s.isEnd = s.progress >= 1;
                }
                if (s.isBeginning && !wasBeginning) s.emit('onReachBeginning', s);
                if (s.isEnd && !wasEnd) s.emit('onReachEnd', s);

                if (s.params.watchSlidesProgress) s.updateSlidesProgress(translate);
                s.emit('onProgress', s, s.progress);
            };
            s.updateActiveIndex = function () {
                var translate = s.rtl ? s.translate : -s.translate;
                var newActiveIndex, i, snapIndex;
                for (i = 0; i < s.slidesGrid.length; i++) {
                    if (typeof s.slidesGrid[i + 1] !== 'undefined') {
                        if (translate >= s.slidesGrid[i] && translate < s.slidesGrid[i + 1] - (s.slidesGrid[i + 1] - s.slidesGrid[i]) / 2) {
                            newActiveIndex = i;
                        }
                        else if (translate >= s.slidesGrid[i] && translate < s.slidesGrid[i + 1]) {
                            newActiveIndex = i + 1;
                        }
                    }
                    else {
                        if (translate >= s.slidesGrid[i]) {
                            newActiveIndex = i;
                        }
                    }
                }
                // Normalize slideIndex
                if (newActiveIndex < 0 || typeof newActiveIndex === 'undefined') newActiveIndex = 0;
                // for (i = 0; i < s.slidesGrid.length; i++) {
                // if (- translate >= s.slidesGrid[i]) {
                // newActiveIndex = i;
                // }
                // }
                snapIndex = Math.floor(newActiveIndex / s.params.slidesPerGroup);
                if (snapIndex >= s.snapGrid.length) snapIndex = s.snapGrid.length - 1;

                if (newActiveIndex === s.activeIndex) {
                    return;
                }
                s.snapIndex = snapIndex;
                s.previousIndex = s.activeIndex;
                s.activeIndex = newActiveIndex;
                s.updateClasses();
            };

            /*=========================
             Classes
             ===========================*/
            s.updateClasses = function () {
                s.slides.removeClass(s.params.slideActiveClass + ' ' + s.params.slideNextClass + ' ' + s.params.slidePrevClass);
                var activeSlide = s.slides.eq(s.activeIndex);
                // Active classes
                activeSlide.addClass(s.params.slideActiveClass);
                activeSlide.next('.' + s.params.slideClass).addClass(s.params.slideNextClass);
                activeSlide.prev('.' + s.params.slideClass).addClass(s.params.slidePrevClass);

                // Pagination
                if (s.bullets && s.bullets.length > 0) {
                    s.bullets.removeClass(s.params.bulletActiveClass);
                    var bulletIndex;
                    if (s.params.loop) {
                        bulletIndex = Math.ceil(s.activeIndex - s.loopedSlides) / s.params.slidesPerGroup;
                        if (bulletIndex > s.slides.length - 1 - s.loopedSlides * 2) {
                            bulletIndex = bulletIndex - (s.slides.length - s.loopedSlides * 2);
                        }
                        if (bulletIndex > s.bullets.length - 1) bulletIndex = bulletIndex - s.bullets.length;
                    }
                    else {
                        if (typeof s.snapIndex !== 'undefined') {
                            bulletIndex = s.snapIndex;
                        }
                        else {
                            bulletIndex = s.activeIndex || 0;
                        }
                    }
                    if (s.paginationContainer.length > 1) {
                        s.bullets.each(function () {
                            if ($(this).index() === bulletIndex) $(this).addClass(s.params.bulletActiveClass);
                        });
                    }
                    else {
                        s.bullets.eq(bulletIndex).addClass(s.params.bulletActiveClass);
                    }
                }

                // Next/active buttons
                if (!s.params.loop) {
                    if (s.params.prevButton) {
                        if (s.isBeginning) {
                            $(s.params.prevButton).addClass(s.params.buttonDisabledClass);
                            if (s.params.a11y && s.a11y) s.a11y.disable($(s.params.prevButton));
                        }
                        else {
                            $(s.params.prevButton).removeClass(s.params.buttonDisabledClass);
                            if (s.params.a11y && s.a11y) s.a11y.enable($(s.params.prevButton));
                        }
                    }
                    if (s.params.nextButton) {
                        if (s.isEnd) {
                            $(s.params.nextButton).addClass(s.params.buttonDisabledClass);
                            if (s.params.a11y && s.a11y) s.a11y.disable($(s.params.nextButton));
                        }
                        else {
                            $(s.params.nextButton).removeClass(s.params.buttonDisabledClass);
                            if (s.params.a11y && s.a11y) s.a11y.enable($(s.params.nextButton));
                        }
                    }
                }
            };

            /*=========================
             Pagination
             ===========================*/
            s.updatePagination = function () {
                if (!s.params.pagination) return;
                if (s.paginationContainer && s.paginationContainer.length > 0) {
                    var bulletsHTML = '';
                    var numberOfBullets = s.params.loop ? Math.ceil((s.slides.length - s.loopedSlides * 2) / s.params.slidesPerGroup) : s.snapGrid.length;
                    for (var i = 0; i < numberOfBullets; i++) {
                        if (s.params.paginationBulletRender) {
                            bulletsHTML += s.params.paginationBulletRender(i, s.params.bulletClass);
                        }
                        else {
                            bulletsHTML += '<' + s.params.paginationElement + ' class="' + s.params.bulletClass + '"></' + s.params.paginationElement + '>';
                        }
                    }
                    s.paginationContainer.html(bulletsHTML);
                    s.bullets = s.paginationContainer.find('.' + s.params.bulletClass);
                    if (s.params.paginationClickable && s.params.a11y && s.a11y) {
                        s.a11y.initPagination();
                    }
                }
            };
            /*=========================
             Common update method
             ===========================*/
            s.update = function (updateTranslate) {
                s.updateContainerSize();
                s.updateSlidesSize();
                s.updateProgress();
                s.updatePagination();
                s.updateClasses();
                if (s.params.scrollbar && s.scrollbar) {
                    s.scrollbar.set();
                }
                function forceSetTranslate() {
                    newTranslate = Math.min(Math.max(s.translate, s.maxTranslate()), s.minTranslate());
                    s.setWrapperTranslate(newTranslate);
                    s.updateActiveIndex();
                    s.updateClasses();
                }

                if (updateTranslate) {
                    var translated, newTranslate;
                    if (s.controller && s.controller.spline) {
                        s.controller.spline = undefined;
                    }
                    if (s.params.freeMode) {
                        forceSetTranslate();
                        if (s.params.autoHeight) {
                            s.updateAutoHeight();
                        }
                    }
                    else {
                        if ((s.params.slidesPerView === 'auto' || s.params.slidesPerView > 1) && s.isEnd && !s.params.centeredSlides) {
                            translated = s.slideTo(s.slides.length - 1, 0, false, true);
                        }
                        else {
                            translated = s.slideTo(s.activeIndex, 0, false, true);
                        }
                        if (!translated) {
                            forceSetTranslate();
                        }
                    }
                }
                else if (s.params.autoHeight) {
                    s.updateAutoHeight();
                }
            };

            /*=========================
             Resize Handler
             ===========================*/
            s.onResize = function (forceUpdatePagination) {
                //Breakpoints
                if (s.params.breakpoints) {
                    s.setBreakpoint();
                }

                // Disable locks on resize
                var allowSwipeToPrev = s.params.allowSwipeToPrev;
                var allowSwipeToNext = s.params.allowSwipeToNext;
                s.params.allowSwipeToPrev = s.params.allowSwipeToNext = true;

                s.updateContainerSize();
                s.updateSlidesSize();
                if (s.params.slidesPerView === 'auto' || s.params.freeMode || forceUpdatePagination) s.updatePagination();
                if (s.params.scrollbar && s.scrollbar) {
                    s.scrollbar.set();
                }
                if (s.controller && s.controller.spline) {
                    s.controller.spline = undefined;
                }
                if (s.params.freeMode) {
                    var newTranslate = Math.min(Math.max(s.translate, s.maxTranslate()), s.minTranslate());
                    s.setWrapperTranslate(newTranslate);
                    s.updateActiveIndex();
                    s.updateClasses();

                    if (s.params.autoHeight) {
                        s.updateAutoHeight();
                    }
                }
                else {
                    s.updateClasses();
                    if ((s.params.slidesPerView === 'auto' || s.params.slidesPerView > 1) && s.isEnd && !s.params.centeredSlides) {
                        s.slideTo(s.slides.length - 1, 0, false, true);
                    }
                    else {
                        s.slideTo(s.activeIndex, 0, false, true);
                    }
                }
                // Return locks after resize
                s.params.allowSwipeToPrev = allowSwipeToPrev;
                s.params.allowSwipeToNext = allowSwipeToNext;
            };

            /*=========================
             Events
             ===========================*/

            //Define Touch Events
            var desktopEvents = ['mousedown', 'mousemove', 'mouseup'];
            if (window.navigator.pointerEnabled) desktopEvents = ['pointerdown', 'pointermove', 'pointerup'];
            else if (window.navigator.msPointerEnabled) desktopEvents = ['MSPointerDown', 'MSPointerMove', 'MSPointerUp'];
            s.touchEvents = {
                start: s.support.touch || !s.params.simulateTouch ? 'touchstart' : desktopEvents[0],
                move: s.support.touch || !s.params.simulateTouch ? 'touchmove' : desktopEvents[1],
                end: s.support.touch || !s.params.simulateTouch ? 'touchend' : desktopEvents[2]
            };


            // WP8 Touch Events Fix
            if (window.navigator.pointerEnabled || window.navigator.msPointerEnabled) {
                (s.params.touchEventsTarget === 'container' ? s.container : s.wrapper).addClass('swiper-wp8-' + s.params.direction);
            }

            // Attach/detach events
            s.initEvents = function (detach) {
                var actionDom = detach ? 'off' : 'on';
                var action = detach ? 'removeEventListener' : 'addEventListener';
                var touchEventsTarget = s.params.touchEventsTarget === 'container' ? s.container[0] : s.wrapper[0];
                var target = s.support.touch ? touchEventsTarget : document;

                var moveCapture = s.params.nested ? true : false;

                //Touch Events
                if (s.browser.ie) {
                    touchEventsTarget[action](s.touchEvents.start, s.onTouchStart, false);
                    target[action](s.touchEvents.move, s.onTouchMove, moveCapture);
                    target[action](s.touchEvents.end, s.onTouchEnd, false);
                }
                else {
                    if (s.support.touch) {
                        touchEventsTarget[action](s.touchEvents.start, s.onTouchStart, false);
                        touchEventsTarget[action](s.touchEvents.move, s.onTouchMove, moveCapture);
                        touchEventsTarget[action](s.touchEvents.end, s.onTouchEnd, false);
                    }
                    if (params.simulateTouch && !s.device.ios && !s.device.android) {
                        touchEventsTarget[action]('mousedown', s.onTouchStart, false);
                        document[action]('mousemove', s.onTouchMove, moveCapture);
                        document[action]('mouseup', s.onTouchEnd, false);
                    }
                }
                window[action]('resize', s.onResize);

                // Next, Prev, Index
                if (s.params.nextButton) {
                    $(s.params.nextButton)[actionDom]('click', s.onClickNext);
                    if (s.params.a11y && s.a11y) $(s.params.nextButton)[actionDom]('keydown', s.a11y.onEnterKey);
                }
                if (s.params.prevButton) {
                    $(s.params.prevButton)[actionDom]('click', s.onClickPrev);
                    if (s.params.a11y && s.a11y) $(s.params.prevButton)[actionDom]('keydown', s.a11y.onEnterKey);
                }
                if (s.params.pagination && s.params.paginationClickable) {
                    $(s.paginationContainer)[actionDom]('click', '.' + s.params.bulletClass, s.onClickIndex);
                    if (s.params.a11y && s.a11y) $(s.paginationContainer)[actionDom]('keydown', '.' + s.params.bulletClass, s.a11y.onEnterKey);
                }

                // Prevent Links Clicks
                if (s.params.preventClicks || s.params.preventClicksPropagation) touchEventsTarget[action]('click', s.preventClicks, true);
            };
            s.attachEvents = function (detach) {
                s.initEvents();
            };
            s.detachEvents = function () {
                s.initEvents(true);
            };

            /*=========================
             Handle Clicks
             ===========================*/
            // Prevent Clicks
            s.allowClick = true;
            s.preventClicks = function (e) {
                if (!s.allowClick) {
                    if (s.params.preventClicks) e.preventDefault();
                    if (s.params.preventClicksPropagation && s.animating) {
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                    }
                }
            };
            // Clicks
            s.onClickNext = function (e) {
                e.preventDefault();
                if (s.isEnd && !s.params.loop) return;
                s.slideNext();
            };
            s.onClickPrev = function (e) {
                e.preventDefault();
                if (s.isBeginning && !s.params.loop) return;
                s.slidePrev();
            };
            s.onClickIndex = function (e) {
                e.preventDefault();
                var index = $(this).index() * s.params.slidesPerGroup;
                if (s.params.loop) index = index + s.loopedSlides;
                s.slideTo(index);
            };

            /*=========================
             Handle Touches
             ===========================*/
            function findElementInEvent(e, selector) {
                var el = $(e.target);
                if (!el.is(selector)) {
                    if (typeof selector === 'string') {
                        el = el.parents(selector);
                    }
                    else if (selector.nodeType) {
                        var found;
                        el.parents().each(function (index, _el) {
                            if (_el === selector) found = selector;
                        });
                        if (!found) return undefined;
                        else return selector;
                    }
                }
                if (el.length === 0) {
                    return undefined;
                }
                return el[0];
            }

            s.updateClickedSlide = function (e) {
                var slide = findElementInEvent(e, '.' + s.params.slideClass);
                var slideFound = false;
                if (slide) {
                    for (var i = 0; i < s.slides.length; i++) {
                        if (s.slides[i] === slide) slideFound = true;
                    }
                }

                if (slide && slideFound) {
                    s.clickedSlide = slide;
                    s.clickedIndex = $(slide).index();
                }
                else {
                    s.clickedSlide = undefined;
                    s.clickedIndex = undefined;
                    return;
                }
                if (s.params.slideToClickedSlide && s.clickedIndex !== undefined && s.clickedIndex !== s.activeIndex) {
                    var slideToIndex = s.clickedIndex,
                        realIndex,
                        duplicatedSlides;
                    if (s.params.loop) {
                        if (s.animating) return;
                        realIndex = $(s.clickedSlide).attr('data-swiper-slide-index');
                        if (s.params.centeredSlides) {
                            if ((slideToIndex < s.loopedSlides - s.params.slidesPerView / 2) || (slideToIndex > s.slides.length - s.loopedSlides + s.params.slidesPerView / 2)) {
                                s.fixLoop();
                                slideToIndex = s.wrapper.children('.' + s.params.slideClass + '[data-swiper-slide-index="' + realIndex + '"]:not(.swiper-slide-duplicate)').eq(0).index();
                                setTimeout(function () {
                                    s.slideTo(slideToIndex);
                                }, 0);
                            }
                            else {
                                s.slideTo(slideToIndex);
                            }
                        }
                        else {
                            if (slideToIndex > s.slides.length - s.params.slidesPerView) {
                                s.fixLoop();
                                slideToIndex = s.wrapper.children('.' + s.params.slideClass + '[data-swiper-slide-index="' + realIndex + '"]:not(.swiper-slide-duplicate)').eq(0).index();
                                setTimeout(function () {
                                    s.slideTo(slideToIndex);
                                }, 0);
                            }
                            else {
                                s.slideTo(slideToIndex);
                            }
                        }
                    }
                    else {
                        s.slideTo(slideToIndex);
                    }
                }
            };

            var isTouched,
                isMoved,
                allowTouchCallbacks,
                touchStartTime,
                isScrolling,
                currentTranslate,
                startTranslate,
                allowThresholdMove,
                // Form elements to match
                formElements = 'input, select, textarea, button',
                // Last click time
                lastClickTime = Date.now(), clickTimeout,
                //Velocities
                velocities = [],
                allowMomentumBounce;

            // Animating Flag
            s.animating = false;

            // Touches information
            s.touches = {
                startX: 0,
                startY: 0,
                currentX: 0,
                currentY: 0,
                diff: 0
            };

            // Touch handlers
            var isTouchEvent, startMoving;
            s.onTouchStart = function (e) {
                if (e.originalEvent) e = e.originalEvent;
                isTouchEvent = e.type === 'touchstart';
                if (!isTouchEvent && 'which' in e && e.which === 3) return;
                if (s.params.noSwiping && findElementInEvent(e, '.' + s.params.noSwipingClass)) {
                    s.allowClick = true;
                    return;
                }
                if (s.params.swipeHandler) {
                    if (!findElementInEvent(e, s.params.swipeHandler)) return;
                }

                var startX = s.touches.currentX = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
                var startY = s.touches.currentY = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;

                // Do NOT start if iOS edge swipe is detected. Otherwise iOS app (UIWebView) cannot swipe-to-go-back anymore
                if (s.device.ios && s.params.iOSEdgeSwipeDetection && startX <= s.params.iOSEdgeSwipeThreshold) {
                    return;
                }

                isTouched = true;
                isMoved = false;
                allowTouchCallbacks = true;
                isScrolling = undefined;
                startMoving = undefined;
                s.touches.startX = startX;
                s.touches.startY = startY;
                touchStartTime = Date.now();
                s.allowClick = true;
                s.updateContainerSize();
                s.swipeDirection = undefined;
                if (s.params.threshold > 0) allowThresholdMove = false;
                if (e.type !== 'touchstart') {
                    var preventDefault = true;
                    if ($(e.target).is(formElements)) preventDefault = false;
                    if (document.activeElement && $(document.activeElement).is(formElements)) {
                        document.activeElement.blur();
                    }
                    if (preventDefault) {
                        e.preventDefault();
                    }
                }
                s.emit('onTouchStart', s, e);
            };

            s.onTouchMove = function (e) {
                if (e.originalEvent) e = e.originalEvent;
                if (isTouchEvent && e.type === 'mousemove') return;
                if (e.preventedByNestedSwiper) return;
                if (s.params.onlyExternal) {
                    // isMoved = true;
                    s.allowClick = false;
                    if (isTouched) {
                        s.touches.startX = s.touches.currentX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
                        s.touches.startY = s.touches.currentY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;
                        touchStartTime = Date.now();
                    }
                    return;
                }
                if (isTouchEvent && document.activeElement) {
                    if (e.target === document.activeElement && $(e.target).is(formElements)) {
                        isMoved = true;
                        s.allowClick = false;
                        return;
                    }
                }
                if (allowTouchCallbacks) {
                    s.emit('onTouchMove', s, e);
                }
                if (e.targetTouches && e.targetTouches.length > 1) return;

                s.touches.currentX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
                s.touches.currentY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;

                if (typeof isScrolling === 'undefined') {
                    var touchAngle = Math.atan2(Math.abs(s.touches.currentY - s.touches.startY), Math.abs(s.touches.currentX - s.touches.startX)) * 180 / Math.PI;
                    isScrolling = isH() ? touchAngle > s.params.touchAngle : (90 - touchAngle > s.params.touchAngle);
                }
                if (isScrolling) {
                    s.emit('onTouchMoveOpposite', s, e);
                }
                if (typeof startMoving === 'undefined' && s.browser.ieTouch) {
                    if (s.touches.currentX !== s.touches.startX || s.touches.currentY !== s.touches.startY) {
                        startMoving = true;
                    }
                }
                if (!isTouched) return;
                if (isScrolling) {
                    isTouched = false;
                    return;
                }
                if (!startMoving && s.browser.ieTouch) {
                    return;
                }
                s.allowClick = false;
                s.emit('onSliderMove', s, e);
                e.preventDefault();
                if (s.params.touchMoveStopPropagation && !s.params.nested) {
                    e.stopPropagation();
                }

                if (!isMoved) {
                    if (params.loop) {
                        s.fixLoop();
                    }
                    startTranslate = s.getWrapperTranslate();
                    s.setWrapperTransition(0);
                    if (s.animating) {
                        s.wrapper.trigger('webkitTransitionEnd transitionend oTransitionEnd MSTransitionEnd msTransitionEnd');
                    }
                    if (s.params.autoplay && s.autoplaying) {
                        if (s.params.autoplayDisableOnInteraction) {
                            s.stopAutoplay();
                        }
                        else {
                            s.pauseAutoplay();
                        }
                    }
                    allowMomentumBounce = false;
                    //Grab Cursor
                    if (s.params.grabCursor) {
                        s.container[0].style.cursor = 'move';
                        s.container[0].style.cursor = '-webkit-grabbing';
                        s.container[0].style.cursor = '-moz-grabbin';
                        s.container[0].style.cursor = 'grabbing';
                    }
                }
                isMoved = true;

                var diff = s.touches.diff = isH() ? s.touches.currentX - s.touches.startX : s.touches.currentY - s.touches.startY;

                diff = diff * s.params.touchRatio;
                if (s.rtl) diff = -diff;

                s.swipeDirection = diff > 0 ? 'prev' : 'next';
                currentTranslate = diff + startTranslate;

                var disableParentSwiper = true;
                if ((diff > 0 && currentTranslate > s.minTranslate())) {
                    disableParentSwiper = false;
                    if (s.params.resistance) currentTranslate = s.minTranslate() - 1 + Math.pow(-s.minTranslate() + startTranslate + diff, s.params.resistanceRatio);
                }
                else if (diff < 0 && currentTranslate < s.maxTranslate()) {
                    disableParentSwiper = false;
                    if (s.params.resistance) currentTranslate = s.maxTranslate() + 1 - Math.pow(s.maxTranslate() - startTranslate - diff, s.params.resistanceRatio);
                }

                if (disableParentSwiper) {
                    e.preventedByNestedSwiper = true;
                }

                // Directions locks
                if (!s.params.allowSwipeToNext && s.swipeDirection === 'next' && currentTranslate < startTranslate) {
                    currentTranslate = startTranslate;
                }
                if (!s.params.allowSwipeToPrev && s.swipeDirection === 'prev' && currentTranslate > startTranslate) {
                    currentTranslate = startTranslate;
                }

                if (!s.params.followFinger) return;

                // Threshold
                if (s.params.threshold > 0) {
                    if (Math.abs(diff) > s.params.threshold || allowThresholdMove) {
                        if (!allowThresholdMove) {
                            allowThresholdMove = true;
                            s.touches.startX = s.touches.currentX;
                            s.touches.startY = s.touches.currentY;
                            currentTranslate = startTranslate;
                            s.touches.diff = isH() ? s.touches.currentX - s.touches.startX : s.touches.currentY - s.touches.startY;
                            return;
                        }
                    }
                    else {
                        currentTranslate = startTranslate;
                        return;
                    }
                }
                // Update active index in free mode
                if (s.params.freeMode || s.params.watchSlidesProgress) {
                    s.updateActiveIndex();
                }
                if (s.params.freeMode) {
                    //Velocity
                    if (velocities.length === 0) {
                        velocities.push({
                            position: s.touches[isH() ? 'startX' : 'startY'],
                            time: touchStartTime
                        });
                    }
                    velocities.push({
                        position: s.touches[isH() ? 'currentX' : 'currentY'],
                        time: (new window.Date()).getTime()
                    });
                }
                // Update progress
                s.updateProgress(currentTranslate);
                // Update translate
                s.setWrapperTranslate(currentTranslate);
            };
            s.onTouchEnd = function (e) {
                if (e.originalEvent) e = e.originalEvent;
                if (allowTouchCallbacks) {
                    s.emit('onTouchEnd', s, e);
                }
                allowTouchCallbacks = false;
                if (!isTouched) return;
                //Return Grab Cursor
                if (s.params.grabCursor && isMoved && isTouched) {
                    s.container[0].style.cursor = 'move';
                    s.container[0].style.cursor = '-webkit-grab';
                    s.container[0].style.cursor = '-moz-grab';
                    s.container[0].style.cursor = 'grab';
                }

                // Time diff
                var touchEndTime = Date.now();
                var timeDiff = touchEndTime - touchStartTime;

                // Tap, doubleTap, Click
                if (s.allowClick) {
                    s.updateClickedSlide(e);
                    s.emit('onTap', s, e);
                    if (timeDiff < 300 && (touchEndTime - lastClickTime) > 300) {
                        if (clickTimeout) clearTimeout(clickTimeout);
                        clickTimeout = setTimeout(function () {
                            if (!s) return;
                            if (s.params.paginationHide && s.paginationContainer.length > 0 && !$(e.target).hasClass(s.params.bulletClass)) {
                                s.paginationContainer.toggleClass(s.params.paginationHiddenClass);
                            }
                            s.emit('onClick', s, e);
                        }, 300);

                    }
                    if (timeDiff < 300 && (touchEndTime - lastClickTime) < 300) {
                        if (clickTimeout) clearTimeout(clickTimeout);
                        s.emit('onDoubleTap', s, e);
                    }
                }

                lastClickTime = Date.now();
                setTimeout(function () {
                    if (s) s.allowClick = true;
                }, 0);

                if (!isTouched || !isMoved || !s.swipeDirection || s.touches.diff === 0 || currentTranslate === startTranslate) {
                    isTouched = isMoved = false;
                    return;
                }
                isTouched = isMoved = false;

                var currentPos;
                if (s.params.followFinger) {
                    currentPos = s.rtl ? s.translate : -s.translate;
                }
                else {
                    currentPos = -currentTranslate;
                }
                if (s.params.freeMode) {
                    if (currentPos < -s.minTranslate()) {
                        s.slideTo(s.activeIndex);
                        return;
                    }
                    else if (currentPos > -s.maxTranslate()) {
                        if (s.slides.length < s.snapGrid.length) {
                            s.slideTo(s.snapGrid.length - 1);
                        }
                        else {
                            s.slideTo(s.slides.length - 1);
                        }
                        return;
                    }

                    if (s.params.freeModeMomentum) {
                        if (velocities.length > 1) {
                            var lastMoveEvent = velocities.pop(), velocityEvent = velocities.pop();

                            var distance = lastMoveEvent.position - velocityEvent.position;
                            var time = lastMoveEvent.time - velocityEvent.time;
                            s.velocity = distance / time;
                            s.velocity = s.velocity / 2;
                            if (Math.abs(s.velocity) < s.params.freeModeMinimumVelocity) {
                                s.velocity = 0;
                            }
                            // this implies that the user stopped moving a finger then released.
                            // There would be no events with distance zero, so the last event is stale.
                            if (time > 150 || (new window.Date().getTime() - lastMoveEvent.time) > 300) {
                                s.velocity = 0;
                            }
                        } else {
                            s.velocity = 0;
                        }

                        velocities.length = 0;
                        var momentumDuration = 1000 * s.params.freeModeMomentumRatio;
                        var momentumDistance = s.velocity * momentumDuration;

                        var newPosition = s.translate + momentumDistance;
                        if (s.rtl) newPosition = -newPosition;
                        var doBounce = false;
                        var afterBouncePosition;
                        var bounceAmount = Math.abs(s.velocity) * 20 * s.params.freeModeMomentumBounceRatio;
                        if (newPosition < s.maxTranslate()) {
                            if (s.params.freeModeMomentumBounce) {
                                if (newPosition + s.maxTranslate() < -bounceAmount) {
                                    newPosition = s.maxTranslate() - bounceAmount;
                                }
                                afterBouncePosition = s.maxTranslate();
                                doBounce = true;
                                allowMomentumBounce = true;
                            }
                            else {
                                newPosition = s.maxTranslate();
                            }
                        }
                        else if (newPosition > s.minTranslate()) {
                            if (s.params.freeModeMomentumBounce) {
                                if (newPosition - s.minTranslate() > bounceAmount) {
                                    newPosition = s.minTranslate() + bounceAmount;
                                }
                                afterBouncePosition = s.minTranslate();
                                doBounce = true;
                                allowMomentumBounce = true;
                            }
                            else {
                                newPosition = s.minTranslate();
                            }
                        }
                        else if (s.params.freeModeSticky) {
                            var j = 0,
                                nextSlide;
                            for (j = 0; j < s.snapGrid.length; j += 1) {
                                if (s.snapGrid[j] > -newPosition) {
                                    nextSlide = j;
                                    break;
                                }

                            }
                            if (Math.abs(s.snapGrid[nextSlide] - newPosition) < Math.abs(s.snapGrid[nextSlide - 1] - newPosition) || s.swipeDirection === 'next') {
                                newPosition = s.snapGrid[nextSlide];
                            } else {
                                newPosition = s.snapGrid[nextSlide - 1];
                            }
                            if (!s.rtl) newPosition = -newPosition;
                        }
                        //Fix duration
                        if (s.velocity !== 0) {
                            if (s.rtl) {
                                momentumDuration = Math.abs((-newPosition - s.translate) / s.velocity);
                            }
                            else {
                                momentumDuration = Math.abs((newPosition - s.translate) / s.velocity);
                            }
                        }
                        else if (s.params.freeModeSticky) {
                            s.slideReset();
                            return;
                        }

                        if (s.params.freeModeMomentumBounce && doBounce) {
                            s.updateProgress(afterBouncePosition);
                            s.setWrapperTransition(momentumDuration);
                            s.setWrapperTranslate(newPosition);
                            s.onTransitionStart();
                            s.animating = true;
                            s.wrapper.transitionEnd(function () {
                                if (!s || !allowMomentumBounce) return;
                                s.emit('onMomentumBounce', s);

                                s.setWrapperTransition(s.params.speed);
                                s.setWrapperTranslate(afterBouncePosition);
                                s.wrapper.transitionEnd(function () {
                                    if (!s) return;
                                    s.onTransitionEnd();
                                });
                            });
                        } else if (s.velocity) {
                            s.updateProgress(newPosition);
                            s.setWrapperTransition(momentumDuration);
                            s.setWrapperTranslate(newPosition);
                            s.onTransitionStart();
                            if (!s.animating) {
                                s.animating = true;
                                s.wrapper.transitionEnd(function () {
                                    if (!s) return;
                                    s.onTransitionEnd();
                                });
                            }

                        } else {
                            s.updateProgress(newPosition);
                        }

                        s.updateActiveIndex();
                    }
                    if (!s.params.freeModeMomentum || timeDiff >= s.params.longSwipesMs) {
                        s.updateProgress();
                        s.updateActiveIndex();
                    }
                    return;
                }

                // Find current slide
                var i, stopIndex = 0, groupSize = s.slidesSizesGrid[0];
                for (i = 0; i < s.slidesGrid.length; i += s.params.slidesPerGroup) {
                    if (typeof s.slidesGrid[i + s.params.slidesPerGroup] !== 'undefined') {
                        if (currentPos >= s.slidesGrid[i] && currentPos < s.slidesGrid[i + s.params.slidesPerGroup]) {
                            stopIndex = i;
                            groupSize = s.slidesGrid[i + s.params.slidesPerGroup] - s.slidesGrid[i];
                        }
                    }
                    else {
                        if (currentPos >= s.slidesGrid[i]) {
                            stopIndex = i;
                            groupSize = s.slidesGrid[s.slidesGrid.length - 1] - s.slidesGrid[s.slidesGrid.length - 2];
                        }
                    }
                }

                // Find current slide size
                var ratio = (currentPos - s.slidesGrid[stopIndex]) / groupSize;

                if (timeDiff > s.params.longSwipesMs) {
                    // Long touches
                    if (!s.params.longSwipes) {
                        s.slideTo(s.activeIndex);
                        return;
                    }
                    if (s.swipeDirection === 'next') {
                        if (ratio >= s.params.longSwipesRatio) s.slideTo(stopIndex + s.params.slidesPerGroup);
                        else s.slideTo(stopIndex);

                    }
                    if (s.swipeDirection === 'prev') {
                        if (ratio > (1 - s.params.longSwipesRatio)) s.slideTo(stopIndex + s.params.slidesPerGroup);
                        else s.slideTo(stopIndex);
                    }
                }
                else {
                    // Short swipes
                    if (!s.params.shortSwipes) {
                        s.slideTo(s.activeIndex);
                        return;
                    }
                    if (s.swipeDirection === 'next') {
                        s.slideTo(stopIndex + s.params.slidesPerGroup);

                    }
                    if (s.swipeDirection === 'prev') {
                        s.slideTo(stopIndex);
                    }
                }
            };
            /*=========================
             Transitions
             ===========================*/
            s._slideTo = function (slideIndex, speed) {
                return s.slideTo(slideIndex, speed, true, true);
            };
            s.slideTo = function (slideIndex, speed, runCallbacks, internal) {
                if (typeof runCallbacks === 'undefined') runCallbacks = true;
                if (typeof slideIndex === 'undefined') slideIndex = 0;
                if (slideIndex < 0) slideIndex = 0;
                s.snapIndex = Math.floor(slideIndex / s.params.slidesPerGroup);
                if (s.snapIndex >= s.snapGrid.length) s.snapIndex = s.snapGrid.length - 1;

                var translate = -s.snapGrid[s.snapIndex];
                // Stop autoplay
                if (s.params.autoplay && s.autoplaying) {
                    if (internal || !s.params.autoplayDisableOnInteraction) {
                        s.pauseAutoplay(speed);
                    }
                    else {
                        s.stopAutoplay();
                    }
                }
                // Update progress
                s.updateProgress(translate);

                // Normalize slideIndex
                for (var i = 0; i < s.slidesGrid.length; i++) {
                    if (-Math.floor(translate * 100) >= Math.floor(s.slidesGrid[i] * 100)) {
                        slideIndex = i;
                    }
                }

                // Directions locks
                if (!s.params.allowSwipeToNext && translate < s.translate && translate < s.minTranslate()) {
                    return false;
                }
                if (!s.params.allowSwipeToPrev && translate > s.translate && translate > s.maxTranslate()) {
                    if ((s.activeIndex || 0) !== slideIndex) return false;
                }

                // Update Index
                if (typeof speed === 'undefined') speed = s.params.speed;
                s.previousIndex = s.activeIndex || 0;
                s.activeIndex = slideIndex;

                if ((s.rtl && -translate === s.translate) || (!s.rtl && translate === s.translate)) {
                    // Update Height
                    if (s.params.autoHeight) {
                        s.updateAutoHeight();
                    }
                    s.updateClasses();
                    if (s.params.effect !== 'slide') {
                        s.setWrapperTranslate(translate);
                    }
                    return false;
                }
                s.updateClasses();
                s.onTransitionStart(runCallbacks);

                if (speed === 0) {
                    s.setWrapperTranslate(translate);
                    s.setWrapperTransition(0);
                    s.onTransitionEnd(runCallbacks);
                }
                else {
                    s.setWrapperTranslate(translate);
                    s.setWrapperTransition(speed);
                    if (!s.animating) {
                        s.animating = true;
                        s.wrapper.transitionEnd(function () {
                            if (!s) return;
                            s.onTransitionEnd(runCallbacks);
                        });
                    }

                }

                return true;
            };

            s.onTransitionStart = function (runCallbacks) {
                if (typeof runCallbacks === 'undefined') runCallbacks = true;
                if (s.params.autoHeight) {
                    s.updateAutoHeight();
                }
                if (s.lazy) s.lazy.onTransitionStart();
                if (runCallbacks) {
                    s.emit('onTransitionStart', s);
                    if (s.activeIndex !== s.previousIndex) {
                        s.emit('onSlideChangeStart', s);
                        _scope.$emit("$ionicSlides.slideChangeStart", {
                            slider: s,
                            activeIndex: s.getSlideDataIndex(s.activeIndex),
                            previousIndex: s.getSlideDataIndex(s.previousIndex)
                        });
                        if (s.activeIndex > s.previousIndex) {
                            s.emit('onSlideNextStart', s);
                        }
                        else {
                            s.emit('onSlidePrevStart', s);
                        }
                    }

                }
            };
            s.onTransitionEnd = function (runCallbacks) {
                s.animating = false;
                s.setWrapperTransition(0);
                if (typeof runCallbacks === 'undefined') runCallbacks = true;
                if (s.lazy) s.lazy.onTransitionEnd();
                if (runCallbacks) {
                    s.emit('onTransitionEnd', s);
                    if (s.activeIndex !== s.previousIndex) {
                        s.emit('onSlideChangeEnd', s);
                        _scope.$emit("$ionicSlides.slideChangeEnd", {
                            slider: s,
                            activeIndex: s.getSlideDataIndex(s.activeIndex),
                            previousIndex: s.getSlideDataIndex(s.previousIndex)
                        });
                        if (s.activeIndex > s.previousIndex) {
                            s.emit('onSlideNextEnd', s);
                        }
                        else {
                            s.emit('onSlidePrevEnd', s);
                        }
                    }
                }
                if (s.params.hashnav && s.hashnav) {
                    s.hashnav.setHash();
                }

            };
            s.slideNext = function (runCallbacks, speed, internal) {
                if (s.params.loop) {
                    if (s.animating) return false;
                    s.fixLoop();
                    var clientLeft = s.container[0].clientLeft;
                    return s.slideTo(s.activeIndex + s.params.slidesPerGroup, speed, runCallbacks, internal);
                }
                else return s.slideTo(s.activeIndex + s.params.slidesPerGroup, speed, runCallbacks, internal);
            };
            s._slideNext = function (speed) {
                return s.slideNext(true, speed, true);
            };
            s.slidePrev = function (runCallbacks, speed, internal) {
                if (s.params.loop) {
                    if (s.animating) return false;
                    s.fixLoop();
                    var clientLeft = s.container[0].clientLeft;
                    return s.slideTo(s.activeIndex - 1, speed, runCallbacks, internal);
                }
                else return s.slideTo(s.activeIndex - 1, speed, runCallbacks, internal);
            };
            s._slidePrev = function (speed) {
                return s.slidePrev(true, speed, true);
            };
            s.slideReset = function (runCallbacks, speed, internal) {
                return s.slideTo(s.activeIndex, speed, runCallbacks);
            };

            /*=========================
             Translate/transition helpers
             ===========================*/
            s.setWrapperTransition = function (duration, byController) {
                s.wrapper.transition(duration);
                if (s.params.effect !== 'slide' && s.effects[s.params.effect]) {
                    s.effects[s.params.effect].setTransition(duration);
                }
                if (s.params.parallax && s.parallax) {
                    s.parallax.setTransition(duration);
                }
                if (s.params.scrollbar && s.scrollbar) {
                    s.scrollbar.setTransition(duration);
                }
                if (s.params.control && s.controller) {
                    s.controller.setTransition(duration, byController);
                }
                s.emit('onSetTransition', s, duration);
            };
            s.setWrapperTranslate = function (translate, updateActiveIndex, byController) {
                var x = 0, y = 0, z = 0;
                if (isH()) {
                    x = s.rtl ? -translate : translate;
                }
                else {
                    y = translate;
                }

                if (s.params.roundLengths) {
                    x = round(x);
                    y = round(y);
                }

                if (!s.params.virtualTranslate) {
                    if (s.support.transforms3d) s.wrapper.transform('translate3d(' + x + 'px, ' + y + 'px, ' + z + 'px)');
                    else s.wrapper.transform('translate(' + x + 'px, ' + y + 'px)');
                }

                s.translate = isH() ? x : y;

                // Check if we need to update progress
                var progress;
                var translatesDiff = s.maxTranslate() - s.minTranslate();
                if (translatesDiff === 0) {
                    progress = 0;
                }
                else {
                    progress = (translate - s.minTranslate()) / (translatesDiff);
                }
                if (progress !== s.progress) {
                    s.updateProgress(translate);
                }

                if (updateActiveIndex) s.updateActiveIndex();
                if (s.params.effect !== 'slide' && s.effects[s.params.effect]) {
                    s.effects[s.params.effect].setTranslate(s.translate);
                }
                if (s.params.parallax && s.parallax) {
                    s.parallax.setTranslate(s.translate);
                }
                if (s.params.scrollbar && s.scrollbar) {
                    s.scrollbar.setTranslate(s.translate);
                }
                if (s.params.control && s.controller) {
                    s.controller.setTranslate(s.translate, byController);
                }
                s.emit('onSetTranslate', s, s.translate);
            };

            s.getTranslate = function (el, axis) {
                var matrix, curTransform, curStyle, transformMatrix;

                // automatic axis detection
                if (typeof axis === 'undefined') {
                    axis = 'x';
                }

                if (s.params.virtualTranslate) {
                    return s.rtl ? -s.translate : s.translate;
                }

                curStyle = window.getComputedStyle(el, null);
                if (window.WebKitCSSMatrix) {
                    curTransform = curStyle.transform || curStyle.webkitTransform;
                    if (curTransform.split(',').length > 6) {
                        curTransform = curTransform.split(', ').map(function (a) {
                            return a.replace(',', '.');
                        }).join(', ');
                    }
                    // Some old versions of Webkit choke when 'none' is passed; pass
                    // empty string instead in this case
                    transformMatrix = new window.WebKitCSSMatrix(curTransform === 'none' ? '' : curTransform);
                }
                else {
                    transformMatrix = curStyle.MozTransform || curStyle.OTransform || curStyle.MsTransform || curStyle.msTransform || curStyle.transform || curStyle.getPropertyValue('transform').replace('translate(', 'matrix(1, 0, 0, 1,');
                    matrix = transformMatrix.toString().split(',');
                }

                if (axis === 'x') {
                    //Latest Chrome and webkits Fix
                    if (window.WebKitCSSMatrix)
                        curTransform = transformMatrix.m41;
                    //Crazy IE10 Matrix
                    else if (matrix.length === 16)
                        curTransform = parseFloat(matrix[12]);
                    //Normal Browsers
                    else
                        curTransform = parseFloat(matrix[4]);
                }
                if (axis === 'y') {
                    //Latest Chrome and webkits Fix
                    if (window.WebKitCSSMatrix)
                        curTransform = transformMatrix.m42;
                    //Crazy IE10 Matrix
                    else if (matrix.length === 16)
                        curTransform = parseFloat(matrix[13]);
                    //Normal Browsers
                    else
                        curTransform = parseFloat(matrix[5]);
                }
                if (s.rtl && curTransform) curTransform = -curTransform;
                return curTransform || 0;
            };
            s.getWrapperTranslate = function (axis) {
                if (typeof axis === 'undefined') {
                    axis = isH() ? 'x' : 'y';
                }
                return s.getTranslate(s.wrapper[0], axis);
            };

            /*=========================
             Observer
             ===========================*/
            s.observers = [];
            function initObserver(target, options) {
                options = options || {};
                // create an observer instance
                var ObserverFunc = window.MutationObserver || window.WebkitMutationObserver;
                var observer = new ObserverFunc(function (mutations) {
                    mutations.forEach(function (mutation) {
                        s.onResize(true);
                        s.emit('onObserverUpdate', s, mutation);
                    });
                });

                observer.observe(target, {
                    attributes: typeof options.attributes === 'undefined' ? true : options.attributes,
                    childList: typeof options.childList === 'undefined' ? true : options.childList,
                    characterData: typeof options.characterData === 'undefined' ? true : options.characterData
                });

                s.observers.push(observer);
            }

            s.initObservers = function () {
                if (s.params.observeParents) {
                    var containerParents = s.container.parents();
                    for (var i = 0; i < containerParents.length; i++) {
                        initObserver(containerParents[i]);
                    }
                }

                // Observe container
                initObserver(s.container[0], {childList: false});

                // Observe wrapper
                initObserver(s.wrapper[0], {attributes: false});
            };
            s.disconnectObservers = function () {
                for (var i = 0; i < s.observers.length; i++) {
                    s.observers[i].disconnect();
                }
                s.observers = [];
            };

            s.updateLoop = function () {
                var currentSlide = s.slides.eq(s.activeIndex);
                if (angular.element(currentSlide).hasClass(s.params.slideDuplicateClass)) {
                    // we're on a duplicate, so slide to the non-duplicate
                    var swiperSlideIndex = angular.element(currentSlide).attr("data-swiper-slide-index");
                    var slides = s.wrapper.children('.' + s.params.slideClass);
                    for (var i = 0; i < slides.length; i++) {
                        if (!angular.element(slides[i]).hasClass(s.params.slideDuplicateClass) && angular.element(slides[i]).attr("data-swiper-slide-index") === swiperSlideIndex) {
                            s.slideTo(i, 0, false, true);
                            break;
                        }
                    }
                    // if we needed to switch slides, we did that.  So, now call the createLoop function internally
                    setTimeout(function () {
                        s.createLoop();
                    }, 50);
                }
            }

            s.getSlideDataIndex = function (slideIndex) {
                // this is an Ionic custom function
                // Swiper loops utilize duplicate DOM elements for slides when in a loop
                // which means that we cannot rely on the actual slide index for our events
                // because index 0 does not necessarily point to index 0
                // and index n+1 does not necessarily point to the expected piece of data
                // therefore, rather than using the actual slide index we should
                // use the data index that swiper includes as an attribute on the dom elements
                // because this is what will be meaningful to the consumer of our events
                var slide = s.slides.eq(slideIndex);
                var attributeIndex = angular.element(slide).attr("data-swiper-slide-index");
                return parseInt(attributeIndex);
            }

            /*=========================
             Loop
             ===========================*/
            // Create looped slides
            s.createLoop = function () {
                //console.log("Slider create loop method");
                //var toRemove = s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass);
                //angular.element(toRemove).remove();
                s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass).remove();

                var slides = s.wrapper.children('.' + s.params.slideClass);

                if (s.params.slidesPerView === 'auto' && !s.params.loopedSlides) s.params.loopedSlides = slides.length;

                s.loopedSlides = parseInt(s.params.loopedSlides || s.params.slidesPerView, 10);
                s.loopedSlides = s.loopedSlides + s.params.loopAdditionalSlides;
                if (s.loopedSlides > slides.length) {
                    s.loopedSlides = slides.length;
                }

                var prependSlides = [], appendSlides = [], i, scope, newNode;
                slides.each(function (index, el) {
                    var slide = $(this);
                    if (index < s.loopedSlides) appendSlides.push(el);
                    if (index < slides.length && index >= slides.length - s.loopedSlides) prependSlides.push(el);
                    slide.attr('data-swiper-slide-index', index);
                });
                for (i = 0; i < appendSlides.length; i++) {

                    newNode = angular.element(appendSlides[i]).clone().addClass(s.params.slideDuplicateClass);
                    newNode.removeAttr('ng-transclude');
                    newNode.removeAttr('ng-repeat');
                    scope = angular.element(appendSlides[i]).scope();
                    newNode = $compile(newNode)(scope);
                    angular.element(s.wrapper).append(newNode);
                    //s.wrapper.append($(appendSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass));
                }
                for (i = prependSlides.length - 1; i >= 0; i--) {
                    //s.wrapper.prepend($(prependSlides[i].cloneNode(true)).addClass(s.params.slideDuplicateClass));

                    newNode = angular.element(prependSlides[i]).clone().addClass(s.params.slideDuplicateClass);
                    newNode.removeAttr('ng-transclude');
                    newNode.removeAttr('ng-repeat');

                    scope = angular.element(prependSlides[i]).scope();
                    newNode = $compile(newNode)(scope);
                    angular.element(s.wrapper).prepend(newNode);
                }
            };
            s.destroyLoop = function () {
                s.wrapper.children('.' + s.params.slideClass + '.' + s.params.slideDuplicateClass).remove();
                s.slides.removeAttr('data-swiper-slide-index');
            };
            s.fixLoop = function () {
                var newIndex;
                //Fix For Negative Oversliding
                if (s.activeIndex < s.loopedSlides) {
                    newIndex = s.slides.length - s.loopedSlides * 3 + s.activeIndex;
                    newIndex = newIndex + s.loopedSlides;
                    s.slideTo(newIndex, 0, false, true);
                }
                //Fix For Positive Oversliding
                else if ((s.params.slidesPerView === 'auto' && s.activeIndex >= s.loopedSlides * 2) || (s.activeIndex > s.slides.length - s.params.slidesPerView * 2)) {
                    newIndex = -s.slides.length + s.activeIndex + s.loopedSlides;
                    newIndex = newIndex + s.loopedSlides;
                    s.slideTo(newIndex, 0, false, true);
                }
            };
            /*=========================
             Append/Prepend/Remove Slides
             ===========================*/
            s.appendSlide = function (slides) {
                if (s.params.loop) {
                    s.destroyLoop();
                }
                if (typeof slides === 'object' && slides.length) {
                    for (var i = 0; i < slides.length; i++) {
                        if (slides[i]) s.wrapper.append(slides[i]);
                    }
                }
                else {
                    s.wrapper.append(slides);
                }
                if (s.params.loop) {
                    s.createLoop();
                }
                if (!(s.params.observer && s.support.observer)) {
                    s.update(true);
                }
            };
            s.prependSlide = function (slides) {
                if (s.params.loop) {
                    s.destroyLoop();
                }
                var newActiveIndex = s.activeIndex + 1;
                if (typeof slides === 'object' && slides.length) {
                    for (var i = 0; i < slides.length; i++) {
                        if (slides[i]) s.wrapper.prepend(slides[i]);
                    }
                    newActiveIndex = s.activeIndex + slides.length;
                }
                else {
                    s.wrapper.prepend(slides);
                }
                if (s.params.loop) {
                    s.createLoop();
                }
                if (!(s.params.observer && s.support.observer)) {
                    s.update(true);
                }
                s.slideTo(newActiveIndex, 0, false);
            };
            s.removeSlide = function (slidesIndexes) {
                if (s.params.loop) {
                    s.destroyLoop();
                    s.slides = s.wrapper.children('.' + s.params.slideClass);
                }
                var newActiveIndex = s.activeIndex,
                    indexToRemove;
                if (typeof slidesIndexes === 'object' && slidesIndexes.length) {
                    for (var i = 0; i < slidesIndexes.length; i++) {
                        indexToRemove = slidesIndexes[i];
                        if (s.slides[indexToRemove]) s.slides.eq(indexToRemove).remove();
                        if (indexToRemove < newActiveIndex) newActiveIndex--;
                    }
                    newActiveIndex = Math.max(newActiveIndex, 0);
                }
                else {
                    indexToRemove = slidesIndexes;
                    if (s.slides[indexToRemove]) s.slides.eq(indexToRemove).remove();
                    if (indexToRemove < newActiveIndex) newActiveIndex--;
                    newActiveIndex = Math.max(newActiveIndex, 0);
                }

                if (s.params.loop) {
                    s.createLoop();
                }

                if (!(s.params.observer && s.support.observer)) {
                    s.update(true);
                }
                if (s.params.loop) {
                    s.slideTo(newActiveIndex + s.loopedSlides, 0, false);
                }
                else {
                    s.slideTo(newActiveIndex, 0, false);
                }

            };
            s.removeAllSlides = function () {
                var slidesIndexes = [];
                for (var i = 0; i < s.slides.length; i++) {
                    slidesIndexes.push(i);
                }
                s.removeSlide(slidesIndexes);
            };


            /*=========================
             Effects
             ===========================*/
            s.effects = {
                fade: {
                    setTranslate: function () {
                        for (var i = 0; i < s.slides.length; i++) {
                            var slide = s.slides.eq(i);
                            var offset = slide[0].swiperSlideOffset;
                            var tx = -offset;
                            if (!s.params.virtualTranslate) tx = tx - s.translate;
                            var ty = 0;
                            if (!isH()) {
                                ty = tx;
                                tx = 0;
                            }
                            var slideOpacity = s.params.fade.crossFade ?
                                Math.max(1 - Math.abs(slide[0].progress), 0) :
                                1 + Math.min(Math.max(slide[0].progress, -1), 0);
                            slide
                                .css({
                                    opacity: slideOpacity
                                })
                                .transform('translate3d(' + tx + 'px, ' + ty + 'px, 0px)');

                        }

                    },
                    setTransition: function (duration) {
                        s.slides.transition(duration);
                        if (s.params.virtualTranslate && duration !== 0) {
                            var eventTriggered = false;
                            s.slides.transitionEnd(function () {
                                if (eventTriggered) return;
                                if (!s) return;
                                eventTriggered = true;
                                s.animating = false;
                                var triggerEvents = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd'];
                                for (var i = 0; i < triggerEvents.length; i++) {
                                    s.wrapper.trigger(triggerEvents[i]);
                                }
                            });
                        }
                    }
                },
                cube: {
                    setTranslate: function () {
                        var wrapperRotate = 0, cubeShadow;
                        if (s.params.cube.shadow) {
                            if (isH()) {
                                cubeShadow = s.wrapper.find('.swiper-cube-shadow');
                                if (cubeShadow.length === 0) {
                                    cubeShadow = $('<div class="swiper-cube-shadow"></div>');
                                    s.wrapper.append(cubeShadow);
                                }
                                cubeShadow.css({height: s.width + 'px'});
                            }
                            else {
                                cubeShadow = s.container.find('.swiper-cube-shadow');
                                if (cubeShadow.length === 0) {
                                    cubeShadow = $('<div class="swiper-cube-shadow"></div>');
                                    s.container.append(cubeShadow);
                                }
                            }
                        }
                        for (var i = 0; i < s.slides.length; i++) {
                            var slide = s.slides.eq(i);
                            var slideAngle = i * 90;
                            var round = Math.floor(slideAngle / 360);
                            if (s.rtl) {
                                slideAngle = -slideAngle;
                                round = Math.floor(-slideAngle / 360);
                            }
                            var progress = Math.max(Math.min(slide[0].progress, 1), -1);
                            var tx = 0, ty = 0, tz = 0;
                            if (i % 4 === 0) {
                                tx = -round * 4 * s.size;
                                tz = 0;
                            }
                            else if ((i - 1) % 4 === 0) {
                                tx = 0;
                                tz = -round * 4 * s.size;
                            }
                            else if ((i - 2) % 4 === 0) {
                                tx = s.size + round * 4 * s.size;
                                tz = s.size;
                            }
                            else if ((i - 3) % 4 === 0) {
                                tx = -s.size;
                                tz = 3 * s.size + s.size * 4 * round;
                            }
                            if (s.rtl) {
                                tx = -tx;
                            }

                            if (!isH()) {
                                ty = tx;
                                tx = 0;
                            }

                            var transform = 'rotateX(' + (isH() ? 0 : -slideAngle) + 'deg) rotateY(' + (isH() ? slideAngle : 0) + 'deg) translate3d(' + tx + 'px, ' + ty + 'px, ' + tz + 'px)';
                            if (progress <= 1 && progress > -1) {
                                wrapperRotate = i * 90 + progress * 90;
                                if (s.rtl) wrapperRotate = -i * 90 - progress * 90;
                            }
                            slide.transform(transform);
                            if (s.params.cube.slideShadows) {
                                //Set shadows
                                var shadowBefore = isH() ? slide.find('.swiper-slide-shadow-left') : slide.find('.swiper-slide-shadow-top');
                                var shadowAfter = isH() ? slide.find('.swiper-slide-shadow-right') : slide.find('.swiper-slide-shadow-bottom');
                                if (shadowBefore.length === 0) {
                                    shadowBefore = $('<div class="swiper-slide-shadow-' + (isH() ? 'left' : 'top') + '"></div>');
                                    slide.append(shadowBefore);
                                }
                                if (shadowAfter.length === 0) {
                                    shadowAfter = $('<div class="swiper-slide-shadow-' + (isH() ? 'right' : 'bottom') + '"></div>');
                                    slide.append(shadowAfter);
                                }
                                var shadowOpacity = slide[0].progress;
                                if (shadowBefore.length) shadowBefore[0].style.opacity = -slide[0].progress;
                                if (shadowAfter.length) shadowAfter[0].style.opacity = slide[0].progress;
                            }
                        }
                        s.wrapper.css({
                            '-webkit-transform-origin': '50% 50% -' + (s.size / 2) + 'px',
                            '-moz-transform-origin': '50% 50% -' + (s.size / 2) + 'px',
                            '-ms-transform-origin': '50% 50% -' + (s.size / 2) + 'px',
                            'transform-origin': '50% 50% -' + (s.size / 2) + 'px'
                        });

                        if (s.params.cube.shadow) {
                            if (isH()) {
                                cubeShadow.transform('translate3d(0px, ' + (s.width / 2 + s.params.cube.shadowOffset) + 'px, ' + (-s.width / 2) + 'px) rotateX(90deg) rotateZ(0deg) scale(' + (s.params.cube.shadowScale) + ')');
                            }
                            else {
                                var shadowAngle = Math.abs(wrapperRotate) - Math.floor(Math.abs(wrapperRotate) / 90) * 90;
                                var multiplier = 1.5 - (Math.sin(shadowAngle * 2 * Math.PI / 360) / 2 + Math.cos(shadowAngle * 2 * Math.PI / 360) / 2);
                                var scale1 = s.params.cube.shadowScale,
                                    scale2 = s.params.cube.shadowScale / multiplier,
                                    offset = s.params.cube.shadowOffset;
                                cubeShadow.transform('scale3d(' + scale1 + ', 1, ' + scale2 + ') translate3d(0px, ' + (s.height / 2 + offset) + 'px, ' + (-s.height / 2 / scale2) + 'px) rotateX(-90deg)');
                            }
                        }
                        var zFactor = (s.isSafari || s.isUiWebView) ? (-s.size / 2) : 0;
                        s.wrapper.transform('translate3d(0px,0,' + zFactor + 'px) rotateX(' + (isH() ? 0 : wrapperRotate) + 'deg) rotateY(' + (isH() ? -wrapperRotate : 0) + 'deg)');
                    },
                    setTransition: function (duration) {
                        s.slides.transition(duration).find('.swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left').transition(duration);
                        if (s.params.cube.shadow && !isH()) {
                            s.container.find('.swiper-cube-shadow').transition(duration);
                        }
                    }
                },
                coverflow: {
                    setTranslate: function () {
                        var transform = s.translate;
                        var center = isH() ? -transform + s.width / 2 : -transform + s.height / 2;
                        var rotate = isH() ? s.params.coverflow.rotate : -s.params.coverflow.rotate;
                        var translate = s.params.coverflow.depth;
                        //Each slide offset from center
                        for (var i = 0, length = s.slides.length; i < length; i++) {
                            var slide = s.slides.eq(i);
                            var slideSize = s.slidesSizesGrid[i];
                            var slideOffset = slide[0].swiperSlideOffset;
                            var offsetMultiplier = (center - slideOffset - slideSize / 2) / slideSize * s.params.coverflow.modifier;

                            var rotateY = isH() ? rotate * offsetMultiplier : 0;
                            var rotateX = isH() ? 0 : rotate * offsetMultiplier;
                            // var rotateZ = 0
                            var translateZ = -translate * Math.abs(offsetMultiplier);

                            var translateY = isH() ? 0 : s.params.coverflow.stretch * (offsetMultiplier);
                            var translateX = isH() ? s.params.coverflow.stretch * (offsetMultiplier) : 0;

                            //Fix for ultra small values
                            if (Math.abs(translateX) < 0.001) translateX = 0;
                            if (Math.abs(translateY) < 0.001) translateY = 0;
                            if (Math.abs(translateZ) < 0.001) translateZ = 0;
                            if (Math.abs(rotateY) < 0.001) rotateY = 0;
                            if (Math.abs(rotateX) < 0.001) rotateX = 0;

                            var slideTransform = 'translate3d(' + translateX + 'px,' + translateY + 'px,' + translateZ + 'px)  rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg)';

                            slide.transform(slideTransform);
                            slide[0].style.zIndex = -Math.abs(Math.round(offsetMultiplier)) + 1;
                            if (s.params.coverflow.slideShadows) {
                                //Set shadows
                                var shadowBefore = isH() ? slide.find('.swiper-slide-shadow-left') : slide.find('.swiper-slide-shadow-top');
                                var shadowAfter = isH() ? slide.find('.swiper-slide-shadow-right') : slide.find('.swiper-slide-shadow-bottom');
                                if (shadowBefore.length === 0) {
                                    shadowBefore = $('<div class="swiper-slide-shadow-' + (isH() ? 'left' : 'top') + '"></div>');
                                    slide.append(shadowBefore);
                                }
                                if (shadowAfter.length === 0) {
                                    shadowAfter = $('<div class="swiper-slide-shadow-' + (isH() ? 'right' : 'bottom') + '"></div>');
                                    slide.append(shadowAfter);
                                }
                                if (shadowBefore.length) shadowBefore[0].style.opacity = offsetMultiplier > 0 ? offsetMultiplier : 0;
                                if (shadowAfter.length) shadowAfter[0].style.opacity = (-offsetMultiplier) > 0 ? -offsetMultiplier : 0;
                            }
                        }

                        //Set correct perspective for IE10
                        if (s.browser.ie) {
                            var ws = s.wrapper[0].style;
                            ws.perspectiveOrigin = center + 'px 50%';
                        }
                    },
                    setTransition: function (duration) {
                        s.slides.transition(duration).find('.swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left').transition(duration);
                    }
                }
            };

            /*=========================
             Images Lazy Loading
             ===========================*/
            s.lazy = {
                initialImageLoaded: false,
                loadImageInSlide: function (index, loadInDuplicate) {
                    if (typeof index === 'undefined') return;
                    if (typeof loadInDuplicate === 'undefined') loadInDuplicate = true;
                    if (s.slides.length === 0) return;

                    var slide = s.slides.eq(index);
                    var img = slide.find('.swiper-lazy:not(.swiper-lazy-loaded):not(.swiper-lazy-loading)');
                    if (slide.hasClass('swiper-lazy') && !slide.hasClass('swiper-lazy-loaded') && !slide.hasClass('swiper-lazy-loading')) {
                        img = img.add(slide[0]);
                    }
                    if (img.length === 0) return;

                    img.each(function () {
                        var _img = $(this);
                        _img.addClass('swiper-lazy-loading');
                        var background = _img.attr('data-background');
                        var src = _img.attr('data-src'),
                            srcset = _img.attr('data-srcset');
                        s.loadImage(_img[0], (src || background), srcset, false, function () {
                            if (background) {
                                _img.css('background-image', 'url(' + background + ')');
                                _img.removeAttr('data-background');
                            }
                            else {
                                if (srcset) {
                                    _img.attr('srcset', srcset);
                                    _img.removeAttr('data-srcset');
                                }
                                if (src) {
                                    _img.attr('src', src);
                                    _img.removeAttr('data-src');
                                }

                            }

                            _img.addClass('swiper-lazy-loaded').removeClass('swiper-lazy-loading');
                            slide.find('.swiper-lazy-preloader, .preloader').remove();
                            if (s.params.loop && loadInDuplicate) {
                                var slideOriginalIndex = slide.attr('data-swiper-slide-index');
                                if (slide.hasClass(s.params.slideDuplicateClass)) {
                                    var originalSlide = s.wrapper.children('[data-swiper-slide-index="' + slideOriginalIndex + '"]:not(.' + s.params.slideDuplicateClass + ')');
                                    s.lazy.loadImageInSlide(originalSlide.index(), false);
                                }
                                else {
                                    var duplicatedSlide = s.wrapper.children('.' + s.params.slideDuplicateClass + '[data-swiper-slide-index="' + slideOriginalIndex + '"]');
                                    s.lazy.loadImageInSlide(duplicatedSlide.index(), false);
                                }
                            }
                            s.emit('onLazyImageReady', s, slide[0], _img[0]);
                        });

                        s.emit('onLazyImageLoad', s, slide[0], _img[0]);
                    });

                },
                load: function () {
                    var i;
                    if (s.params.watchSlidesVisibility) {
                        s.wrapper.children('.' + s.params.slideVisibleClass).each(function () {
                            s.lazy.loadImageInSlide($(this).index());
                        });
                    }
                    else {
                        if (s.params.slidesPerView > 1) {
                            for (i = s.activeIndex; i < s.activeIndex + s.params.slidesPerView; i++) {
                                if (s.slides[i]) s.lazy.loadImageInSlide(i);
                            }
                        }
                        else {
                            s.lazy.loadImageInSlide(s.activeIndex);
                        }
                    }
                    if (s.params.lazyLoadingInPrevNext) {
                        if (s.params.slidesPerView > 1) {
                            // Next Slides
                            for (i = s.activeIndex + s.params.slidesPerView; i < s.activeIndex + s.params.slidesPerView + s.params.slidesPerView; i++) {
                                if (s.slides[i]) s.lazy.loadImageInSlide(i);
                            }
                            // Prev Slides
                            for (i = s.activeIndex - s.params.slidesPerView; i < s.activeIndex; i++) {
                                if (s.slides[i]) s.lazy.loadImageInSlide(i);
                            }
                        }
                        else {
                            var nextSlide = s.wrapper.children('.' + s.params.slideNextClass);
                            if (nextSlide.length > 0) s.lazy.loadImageInSlide(nextSlide.index());

                            var prevSlide = s.wrapper.children('.' + s.params.slidePrevClass);
                            if (prevSlide.length > 0) s.lazy.loadImageInSlide(prevSlide.index());
                        }
                    }
                },
                onTransitionStart: function () {
                    if (s.params.lazyLoading) {
                        if (s.params.lazyLoadingOnTransitionStart || (!s.params.lazyLoadingOnTransitionStart && !s.lazy.initialImageLoaded)) {
                            s.lazy.load();
                        }
                    }
                },
                onTransitionEnd: function () {
                    if (s.params.lazyLoading && !s.params.lazyLoadingOnTransitionStart) {
                        s.lazy.load();
                    }
                }
            };


            /*=========================
             Scrollbar
             ===========================*/
            s.scrollbar = {
                isTouched: false,
                setDragPosition: function (e) {
                    var sb = s.scrollbar;
                    var x = 0, y = 0;
                    var translate;
                    var pointerPosition = isH() ?
                        ((e.type === 'touchstart' || e.type === 'touchmove') ? e.targetTouches[0].pageX : e.pageX || e.clientX) :
                        ((e.type === 'touchstart' || e.type === 'touchmove') ? e.targetTouches[0].pageY : e.pageY || e.clientY);
                    var position = (pointerPosition) - sb.track.offset()[isH() ? 'left' : 'top'] - sb.dragSize / 2;
                    var positionMin = -s.minTranslate() * sb.moveDivider;
                    var positionMax = -s.maxTranslate() * sb.moveDivider;
                    if (position < positionMin) {
                        position = positionMin;
                    }
                    else if (position > positionMax) {
                        position = positionMax;
                    }
                    position = -position / sb.moveDivider;
                    s.updateProgress(position);
                    s.setWrapperTranslate(position, true);
                },
                dragStart: function (e) {
                    var sb = s.scrollbar;
                    sb.isTouched = true;
                    e.preventDefault();
                    e.stopPropagation();

                    sb.setDragPosition(e);
                    clearTimeout(sb.dragTimeout);

                    sb.track.transition(0);
                    if (s.params.scrollbarHide) {
                        sb.track.css('opacity', 1);
                    }
                    s.wrapper.transition(100);
                    sb.drag.transition(100);
                    s.emit('onScrollbarDragStart', s);
                },
                dragMove: function (e) {
                    var sb = s.scrollbar;
                    if (!sb.isTouched) return;
                    if (e.preventDefault) e.preventDefault();
                    else e.returnValue = false;
                    sb.setDragPosition(e);
                    s.wrapper.transition(0);
                    sb.track.transition(0);
                    sb.drag.transition(0);
                    s.emit('onScrollbarDragMove', s);
                },
                dragEnd: function (e) {
                    var sb = s.scrollbar;
                    if (!sb.isTouched) return;
                    sb.isTouched = false;
                    if (s.params.scrollbarHide) {
                        clearTimeout(sb.dragTimeout);
                        sb.dragTimeout = setTimeout(function () {
                            sb.track.css('opacity', 0);
                            sb.track.transition(400);
                        }, 1000);

                    }
                    s.emit('onScrollbarDragEnd', s);
                    if (s.params.scrollbarSnapOnRelease) {
                        s.slideReset();
                    }
                },
                enableDraggable: function () {
                    var sb = s.scrollbar;
                    var target = s.support.touch ? sb.track : document;
                    $(sb.track).on(s.touchEvents.start, sb.dragStart);
                    $(target).on(s.touchEvents.move, sb.dragMove);
                    $(target).on(s.touchEvents.end, sb.dragEnd);
                },
                disableDraggable: function () {
                    var sb = s.scrollbar;
                    var target = s.support.touch ? sb.track : document;
                    $(sb.track).off(s.touchEvents.start, sb.dragStart);
                    $(target).off(s.touchEvents.move, sb.dragMove);
                    $(target).off(s.touchEvents.end, sb.dragEnd);
                },
                set: function () {
                    if (!s.params.scrollbar) return;
                    var sb = s.scrollbar;
                    sb.track = $(s.params.scrollbar);
                    sb.drag = sb.track.find('.swiper-scrollbar-drag');
                    if (sb.drag.length === 0) {
                        sb.drag = $('<div class="swiper-scrollbar-drag"></div>');
                        sb.track.append(sb.drag);
                    }
                    sb.drag[0].style.width = '';
                    sb.drag[0].style.height = '';
                    sb.trackSize = isH() ? sb.track[0].offsetWidth : sb.track[0].offsetHeight;

                    sb.divider = s.size / s.virtualSize;
                    sb.moveDivider = sb.divider * (sb.trackSize / s.size);
                    sb.dragSize = sb.trackSize * sb.divider;

                    if (isH()) {
                        sb.drag[0].style.width = sb.dragSize + 'px';
                    }
                    else {
                        sb.drag[0].style.height = sb.dragSize + 'px';
                    }

                    if (sb.divider >= 1) {
                        sb.track[0].style.display = 'none';
                    }
                    else {
                        sb.track[0].style.display = '';
                    }
                    if (s.params.scrollbarHide) {
                        sb.track[0].style.opacity = 0;
                    }
                },
                setTranslate: function () {
                    if (!s.params.scrollbar) return;
                    var diff;
                    var sb = s.scrollbar;
                    var translate = s.translate || 0;
                    var newPos;

                    var newSize = sb.dragSize;
                    newPos = (sb.trackSize - sb.dragSize) * s.progress;
                    if (s.rtl && isH()) {
                        newPos = -newPos;
                        if (newPos > 0) {
                            newSize = sb.dragSize - newPos;
                            newPos = 0;
                        }
                        else if (-newPos + sb.dragSize > sb.trackSize) {
                            newSize = sb.trackSize + newPos;
                        }
                    }
                    else {
                        if (newPos < 0) {
                            newSize = sb.dragSize + newPos;
                            newPos = 0;
                        }
                        else if (newPos + sb.dragSize > sb.trackSize) {
                            newSize = sb.trackSize - newPos;
                        }
                    }
                    if (isH()) {
                        if (s.support.transforms3d) {
                            sb.drag.transform('translate3d(' + (newPos) + 'px, 0, 0)');
                        }
                        else {
                            sb.drag.transform('translateX(' + (newPos) + 'px)');
                        }
                        sb.drag[0].style.width = newSize + 'px';
                    }
                    else {
                        if (s.support.transforms3d) {
                            sb.drag.transform('translate3d(0px, ' + (newPos) + 'px, 0)');
                        }
                        else {
                            sb.drag.transform('translateY(' + (newPos) + 'px)');
                        }
                        sb.drag[0].style.height = newSize + 'px';
                    }
                    if (s.params.scrollbarHide) {
                        clearTimeout(sb.timeout);
                        sb.track[0].style.opacity = 1;
                        sb.timeout = setTimeout(function () {
                            sb.track[0].style.opacity = 0;
                            sb.track.transition(400);
                        }, 1000);
                    }
                },
                setTransition: function (duration) {
                    if (!s.params.scrollbar) return;
                    s.scrollbar.drag.transition(duration);
                }
            };

            /*=========================
             Controller
             ===========================*/
            s.controller = {
                LinearSpline: function (x, y) {
                    this.x = x;
                    this.y = y;
                    this.lastIndex = x.length - 1;
                    // Given an x value (x2), return the expected y2 value:
                    // (x1,y1) is the known point before given value,
                    // (x3,y3) is the known point after given value.
                    var i1, i3;
                    var l = this.x.length;

                    this.interpolate = function (x2) {
                        if (!x2) return 0;

                        // Get the indexes of x1 and x3 (the array indexes before and after given x2):
                        i3 = binarySearch(this.x, x2);
                        i1 = i3 - 1;

                        // We have our indexes i1 & i3, so we can calculate already:
                        // y2 := ((x2−x1) × (y3−y1)) ÷ (x3−x1) + y1
                        return ((x2 - this.x[i1]) * (this.y[i3] - this.y[i1])) / (this.x[i3] - this.x[i1]) + this.y[i1];
                    };

                    var binarySearch = (function () {
                        var maxIndex, minIndex, guess;
                        return function (array, val) {
                            minIndex = -1;
                            maxIndex = array.length;
                            while (maxIndex - minIndex > 1)
                                if (array[guess = maxIndex + minIndex >> 1] <= val) {
                                    minIndex = guess;
                                } else {
                                    maxIndex = guess;
                                }
                            return maxIndex;
                        };
                    })();
                },
                //xxx: for now i will just save one spline function to to
                getInterpolateFunction: function (c) {
                    if (!s.controller.spline) s.controller.spline = s.params.loop ?
                        new s.controller.LinearSpline(s.slidesGrid, c.slidesGrid) :
                        new s.controller.LinearSpline(s.snapGrid, c.snapGrid);
                },
                setTranslate: function (translate, byController) {
                    var controlled = s.params.control;
                    var multiplier, controlledTranslate;

                    function setControlledTranslate(c) {
                        // this will create an Interpolate function based on the snapGrids
                        // x is the Grid of the scrolled scroller and y will be the controlled scroller
                        // it makes sense to create this only once and recall it for the interpolation
                        // the function does a lot of value caching for performance
                        translate = c.rtl && c.params.direction === 'horizontal' ? -s.translate : s.translate;
                        if (s.params.controlBy === 'slide') {
                            s.controller.getInterpolateFunction(c);
                            // i am not sure why the values have to be multiplicated this way, tried to invert the snapGrid
                            // but it did not work out
                            controlledTranslate = -s.controller.spline.interpolate(-translate);
                        }

                        if (!controlledTranslate || s.params.controlBy === 'container') {
                            multiplier = (c.maxTranslate() - c.minTranslate()) / (s.maxTranslate() - s.minTranslate());
                            controlledTranslate = (translate - s.minTranslate()) * multiplier + c.minTranslate();
                        }

                        if (s.params.controlInverse) {
                            controlledTranslate = c.maxTranslate() - controlledTranslate;
                        }
                        c.updateProgress(controlledTranslate);
                        c.setWrapperTranslate(controlledTranslate, false, s);
                        c.updateActiveIndex();
                    }

                    if (s.isArray(controlled)) {
                        for (var i = 0; i < controlled.length; i++) {
                            if (controlled[i] !== byController && controlled[i] instanceof Swiper) {
                                setControlledTranslate(controlled[i]);
                            }
                        }
                    }
                    else if (controlled instanceof Swiper && byController !== controlled) {

                        setControlledTranslate(controlled);
                    }
                },
                setTransition: function (duration, byController) {
                    var controlled = s.params.control;
                    var i;

                    function setControlledTransition(c) {
                        c.setWrapperTransition(duration, s);
                        if (duration !== 0) {
                            c.onTransitionStart();
                            c.wrapper.transitionEnd(function () {
                                if (!controlled) return;
                                if (c.params.loop && s.params.controlBy === 'slide') {
                                    c.fixLoop();
                                }
                                c.onTransitionEnd();

                            });
                        }
                    }

                    if (s.isArray(controlled)) {
                        for (i = 0; i < controlled.length; i++) {
                            if (controlled[i] !== byController && controlled[i] instanceof Swiper) {
                                setControlledTransition(controlled[i]);
                            }
                        }
                    }
                    else if (controlled instanceof Swiper && byController !== controlled) {
                        setControlledTransition(controlled);
                    }
                }
            };

            /*=========================
             Hash Navigation
             ===========================*/
            s.hashnav = {
                init: function () {
                    if (!s.params.hashnav) return;
                    s.hashnav.initialized = true;
                    var hash = document.location.hash.replace('#', '');
                    if (!hash) return;
                    var speed = 0;
                    for (var i = 0, length = s.slides.length; i < length; i++) {
                        var slide = s.slides.eq(i);
                        var slideHash = slide.attr('data-hash');
                        if (slideHash === hash && !slide.hasClass(s.params.slideDuplicateClass)) {
                            var index = slide.index();
                            s.slideTo(index, speed, s.params.runCallbacksOnInit, true);
                        }
                    }
                },
                setHash: function () {
                    if (!s.hashnav.initialized || !s.params.hashnav) return;
                    document.location.hash = s.slides.eq(s.activeIndex).attr('data-hash') || '';
                }
            };

            /*=========================
             Keyboard Control
             ===========================*/
            function handleKeyboard(e) {
                if (e.originalEvent) e = e.originalEvent; //jquery fix
                var kc = e.keyCode || e.charCode;
                // Directions locks
                if (!s.params.allowSwipeToNext && (isH() && kc === 39 || !isH() && kc === 40)) {
                    return false;
                }
                if (!s.params.allowSwipeToPrev && (isH() && kc === 37 || !isH() && kc === 38)) {
                    return false;
                }
                if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) {
                    return;
                }
                if (document.activeElement && document.activeElement.nodeName && (document.activeElement.nodeName.toLowerCase() === 'input' || document.activeElement.nodeName.toLowerCase() === 'textarea')) {
                    return;
                }
                if (kc === 37 || kc === 39 || kc === 38 || kc === 40) {
                    var inView = false;
                    //Check that swiper should be inside of visible area of window
                    if (s.container.parents('.swiper-slide').length > 0 && s.container.parents('.swiper-slide-active').length === 0) {
                        return;
                    }
                    var windowScroll = {
                        left: window.pageXOffset,
                        top: window.pageYOffset
                    };
                    var windowWidth = window.innerWidth;
                    var windowHeight = window.innerHeight;
                    var swiperOffset = s.container.offset();
                    if (s.rtl) swiperOffset.left = swiperOffset.left - s.container[0].scrollLeft;
                    var swiperCoord = [
                        [swiperOffset.left, swiperOffset.top],
                        [swiperOffset.left + s.width, swiperOffset.top],
                        [swiperOffset.left, swiperOffset.top + s.height],
                        [swiperOffset.left + s.width, swiperOffset.top + s.height]
                    ];
                    for (var i = 0; i < swiperCoord.length; i++) {
                        var point = swiperCoord[i];
                        if (
                            point[0] >= windowScroll.left && point[0] <= windowScroll.left + windowWidth &&
                            point[1] >= windowScroll.top && point[1] <= windowScroll.top + windowHeight
                        ) {
                            inView = true;
                        }

                    }
                    if (!inView) return;
                }
                if (isH()) {
                    if (kc === 37 || kc === 39) {
                        if (e.preventDefault) e.preventDefault();
                        else e.returnValue = false;
                    }
                    if ((kc === 39 && !s.rtl) || (kc === 37 && s.rtl)) s.slideNext();
                    if ((kc === 37 && !s.rtl) || (kc === 39 && s.rtl)) s.slidePrev();
                }
                else {
                    if (kc === 38 || kc === 40) {
                        if (e.preventDefault) e.preventDefault();
                        else e.returnValue = false;
                    }
                    if (kc === 40) s.slideNext();
                    if (kc === 38) s.slidePrev();
                }
            }

            s.disableKeyboardControl = function () {
                s.params.keyboardControl = false;
                $(document).off('keydown', handleKeyboard);
            };
            s.enableKeyboardControl = function () {
                s.params.keyboardControl = true;
                $(document).on('keydown', handleKeyboard);
            };


            /*=========================
             Mousewheel Control
             ===========================*/
            s.mousewheel = {
                event: false,
                lastScrollTime: (new window.Date()).getTime()
            };
            if (s.params.mousewheelControl) {
                try {
                    new window.WheelEvent('wheel');
                    s.mousewheel.event = 'wheel';
                } catch (e) {
                }

                if (!s.mousewheel.event && document.onmousewheel !== undefined) {
                    s.mousewheel.event = 'mousewheel';
                }
                if (!s.mousewheel.event) {
                    s.mousewheel.event = 'DOMMouseScroll';
                }
            }
            function handleMousewheel(e) {
                if (e.originalEvent) e = e.originalEvent; //jquery fix
                var we = s.mousewheel.event;
                var delta = 0;
                var rtlFactor = s.rtl ? -1 : 1;
                //Opera & IE
                if (e.detail) delta = -e.detail;
                //WebKits
                else if (we === 'mousewheel') {
                    if (s.params.mousewheelForceToAxis) {
                        if (isH()) {
                            if (Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY)) delta = e.wheelDeltaX * rtlFactor;
                            else return;
                        }
                        else {
                            if (Math.abs(e.wheelDeltaY) > Math.abs(e.wheelDeltaX)) delta = e.wheelDeltaY;
                            else return;
                        }
                    }
                    else {
                        delta = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) ? -e.wheelDeltaX * rtlFactor : -e.wheelDeltaY;
                    }
                }
                //Old FireFox
                else if (we === 'DOMMouseScroll') delta = -e.detail;
                //New FireFox
                else if (we === 'wheel') {
                    if (s.params.mousewheelForceToAxis) {
                        if (isH()) {
                            if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) delta = -e.deltaX * rtlFactor;
                            else return;
                        }
                        else {
                            if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) delta = -e.deltaY;
                            else return;
                        }
                    }
                    else {
                        delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? -e.deltaX * rtlFactor : -e.deltaY;
                    }
                }
                if (delta === 0) return;

                if (s.params.mousewheelInvert) delta = -delta;

                if (!s.params.freeMode) {
                    if ((new window.Date()).getTime() - s.mousewheel.lastScrollTime > 60) {
                        if (delta < 0) {
                            if ((!s.isEnd || s.params.loop) && !s.animating) s.slideNext();
                            else if (s.params.mousewheelReleaseOnEdges) return true;
                        }
                        else {
                            if ((!s.isBeginning || s.params.loop) && !s.animating) s.slidePrev();
                            else if (s.params.mousewheelReleaseOnEdges) return true;
                        }
                    }
                    s.mousewheel.lastScrollTime = (new window.Date()).getTime();

                }
                else {
                    //Freemode or scrollContainer:
                    var position = s.getWrapperTranslate() + delta * s.params.mousewheelSensitivity;
                    var wasBeginning = s.isBeginning,
                        wasEnd = s.isEnd;

                    if (position >= s.minTranslate()) position = s.minTranslate();
                    if (position <= s.maxTranslate()) position = s.maxTranslate();

                    s.setWrapperTransition(0);
                    s.setWrapperTranslate(position);
                    s.updateProgress();
                    s.updateActiveIndex();

                    if (!wasBeginning && s.isBeginning || !wasEnd && s.isEnd) {
                        s.updateClasses();
                    }

                    if (s.params.freeModeSticky) {
                        clearTimeout(s.mousewheel.timeout);
                        s.mousewheel.timeout = setTimeout(function () {
                            s.slideReset();
                        }, 300);
                    }

                    // Return page scroll on edge positions
                    if (position === 0 || position === s.maxTranslate()) return;
                }
                if (s.params.autoplay) s.stopAutoplay();

                if (e.preventDefault) e.preventDefault();
                else e.returnValue = false;
                return false;
            }

            s.disableMousewheelControl = function () {
                if (!s.mousewheel.event) return false;
                s.container.off(s.mousewheel.event, handleMousewheel);
                return true;
            };

            s.enableMousewheelControl = function () {
                if (!s.mousewheel.event) return false;
                s.container.on(s.mousewheel.event, handleMousewheel);
                return true;
            };


            /*=========================
             Parallax
             ===========================*/
            function setParallaxTransform(el, progress) {
                el = $(el);
                var p, pX, pY;
                var rtlFactor = s.rtl ? -1 : 1;

                p = el.attr('data-swiper-parallax') || '0';
                pX = el.attr('data-swiper-parallax-x');
                pY = el.attr('data-swiper-parallax-y');
                if (pX || pY) {
                    pX = pX || '0';
                    pY = pY || '0';
                }
                else {
                    if (isH()) {
                        pX = p;
                        pY = '0';
                    }
                    else {
                        pY = p;
                        pX = '0';
                    }
                }

                if ((pX).indexOf('%') >= 0) {
                    pX = parseInt(pX, 10) * progress * rtlFactor + '%';
                }
                else {
                    pX = pX * progress * rtlFactor + 'px';
                }
                if ((pY).indexOf('%') >= 0) {
                    pY = parseInt(pY, 10) * progress + '%';
                }
                else {
                    pY = pY * progress + 'px';
                }

                el.transform('translate3d(' + pX + ', ' + pY + ',0px)');
            }

            s.parallax = {
                setTranslate: function () {
                    s.container.children('[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y]').each(function () {
                        setParallaxTransform(this, s.progress);

                    });
                    s.slides.each(function () {
                        var slide = $(this);
                        slide.find('[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y]').each(function () {
                            var progress = Math.min(Math.max(slide[0].progress, -1), 1);
                            setParallaxTransform(this, progress);
                        });
                    });
                },
                setTransition: function (duration) {
                    if (typeof duration === 'undefined') duration = s.params.speed;
                    s.container.find('[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y]').each(function () {
                        var el = $(this);
                        var parallaxDuration = parseInt(el.attr('data-swiper-parallax-duration'), 10) || duration;
                        if (duration === 0) parallaxDuration = 0;
                        el.transition(parallaxDuration);
                    });
                }
            };


            /*=========================
             Plugins API. Collect all and init all plugins
             ===========================*/
            s._plugins = [];
            for (var plugin in s.plugins) {
                var p = s.plugins[plugin](s, s.params[plugin]);
                if (p) s._plugins.push(p);
            }
            // Method to call all plugins event/method
            s.callPlugins = function (eventName) {
                for (var i = 0; i < s._plugins.length; i++) {
                    if (eventName in s._plugins[i]) {
                        s._plugins[i][eventName](arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
                    }
                }
            };

            /*=========================
             Events/Callbacks/Plugins Emitter
             ===========================*/
            function normalizeEventName(eventName) {
                if (eventName.indexOf('on') !== 0) {
                    if (eventName[0] !== eventName[0].toUpperCase()) {
                        eventName = 'on' + eventName[0].toUpperCase() + eventName.substring(1);
                    }
                    else {
                        eventName = 'on' + eventName;
                    }
                }
                return eventName;
            }

            s.emitterEventListeners = {};
            s.emit = function (eventName) {
                // Trigger callbacks
                if (s.params[eventName]) {
                    s.params[eventName](arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
                }
                var i;
                // Trigger events
                if (s.emitterEventListeners[eventName]) {
                    for (i = 0; i < s.emitterEventListeners[eventName].length; i++) {
                        s.emitterEventListeners[eventName][i](arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
                    }
                }
                // Trigger plugins
                if (s.callPlugins) s.callPlugins(eventName, arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
            };
            s.on = function (eventName, handler) {
                eventName = normalizeEventName(eventName);
                if (!s.emitterEventListeners[eventName]) s.emitterEventListeners[eventName] = [];
                s.emitterEventListeners[eventName].push(handler);
                return s;
            };
            s.off = function (eventName, handler) {
                var i;
                eventName = normalizeEventName(eventName);
                if (typeof handler === 'undefined') {
                    // Remove all handlers for such event
                    s.emitterEventListeners[eventName] = [];
                    return s;
                }
                if (!s.emitterEventListeners[eventName] || s.emitterEventListeners[eventName].length === 0) return;
                for (i = 0; i < s.emitterEventListeners[eventName].length; i++) {
                    if (s.emitterEventListeners[eventName][i] === handler) s.emitterEventListeners[eventName].splice(i, 1);
                }
                return s;
            };
            s.once = function (eventName, handler) {
                eventName = normalizeEventName(eventName);
                var _handler = function () {
                    handler(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
                    s.off(eventName, _handler);
                };
                s.on(eventName, _handler);
                return s;
            };

            // Accessibility tools
            s.a11y = {
                makeFocusable: function ($el) {
                    $el.attr('tabIndex', '0');
                    return $el;
                },
                addRole: function ($el, role) {
                    $el.attr('role', role);
                    return $el;
                },

                addLabel: function ($el, label) {
                    $el.attr('aria-label', label);
                    return $el;
                },

                disable: function ($el) {
                    $el.attr('aria-disabled', true);
                    return $el;
                },

                enable: function ($el) {
                    $el.attr('aria-disabled', false);
                    return $el;
                },

                onEnterKey: function (event) {
                    if (event.keyCode !== 13) return;
                    if ($(event.target).is(s.params.nextButton)) {
                        s.onClickNext(event);
                        if (s.isEnd) {
                            s.a11y.notify(s.params.lastSlideMessage);
                        }
                        else {
                            s.a11y.notify(s.params.nextSlideMessage);
                        }
                    }
                    else if ($(event.target).is(s.params.prevButton)) {
                        s.onClickPrev(event);
                        if (s.isBeginning) {
                            s.a11y.notify(s.params.firstSlideMessage);
                        }
                        else {
                            s.a11y.notify(s.params.prevSlideMessage);
                        }
                    }
                    if ($(event.target).is('.' + s.params.bulletClass)) {
                        $(event.target)[0].click();
                    }
                },

                liveRegion: $('<span class="swiper-notification" aria-live="assertive" aria-atomic="true"></span>'),

                notify: function (message) {
                    var notification = s.a11y.liveRegion;
                    if (notification.length === 0) return;
                    notification.html('');
                    notification.html(message);
                },
                init: function () {
                    // Setup accessibility
                    if (s.params.nextButton) {
                        var nextButton = $(s.params.nextButton);
                        s.a11y.makeFocusable(nextButton);
                        s.a11y.addRole(nextButton, 'button');
                        s.a11y.addLabel(nextButton, s.params.nextSlideMessage);
                    }
                    if (s.params.prevButton) {
                        var prevButton = $(s.params.prevButton);
                        s.a11y.makeFocusable(prevButton);
                        s.a11y.addRole(prevButton, 'button');
                        s.a11y.addLabel(prevButton, s.params.prevSlideMessage);
                    }

                    $(s.container).append(s.a11y.liveRegion);
                },
                initPagination: function () {
                    if (s.params.pagination && s.params.paginationClickable && s.bullets && s.bullets.length) {
                        s.bullets.each(function () {
                            var bullet = $(this);
                            s.a11y.makeFocusable(bullet);
                            s.a11y.addRole(bullet, 'button');
                            s.a11y.addLabel(bullet, s.params.paginationBulletMessage.replace(/{{index}}/, bullet.index() + 1));
                        });
                    }
                },
                destroy: function () {
                    if (s.a11y.liveRegion && s.a11y.liveRegion.length > 0) s.a11y.liveRegion.remove();
                }
            };


            /*=========================
             Init/Destroy
             ===========================*/
            s.init = function () {
                if (s.params.loop) s.createLoop();
                s.updateContainerSize();
                s.updateSlidesSize();
                s.updatePagination();
                if (s.params.scrollbar && s.scrollbar) {
                    s.scrollbar.set();
                    if (s.params.scrollbarDraggable) {
                        s.scrollbar.enableDraggable();
                    }
                }
                if (s.params.effect !== 'slide' && s.effects[s.params.effect]) {
                    if (!s.params.loop) s.updateProgress();
                    s.effects[s.params.effect].setTranslate();
                }
                if (s.params.loop) {
                    s.slideTo(s.params.initialSlide + s.loopedSlides, 0, s.params.runCallbacksOnInit);
                }
                else {
                    s.slideTo(s.params.initialSlide, 0, s.params.runCallbacksOnInit);
                    if (s.params.initialSlide === 0) {
                        if (s.parallax && s.params.parallax) s.parallax.setTranslate();
                        if (s.lazy && s.params.lazyLoading) {
                            s.lazy.load();
                            s.lazy.initialImageLoaded = true;
                        }
                    }
                }
                s.attachEvents();
                if (s.params.observer && s.support.observer) {
                    s.initObservers();
                }
                if (s.params.preloadImages && !s.params.lazyLoading) {
                    s.preloadImages();
                }
                if (s.params.autoplay) {
                    s.startAutoplay();
                }
                if (s.params.keyboardControl) {
                    if (s.enableKeyboardControl) s.enableKeyboardControl();
                }
                if (s.params.mousewheelControl) {
                    if (s.enableMousewheelControl) s.enableMousewheelControl();
                }
                if (s.params.hashnav) {
                    if (s.hashnav) s.hashnav.init();
                }
                if (s.params.a11y && s.a11y) s.a11y.init();
                s.emit('onInit', s);
            };

            // Cleanup dynamic styles
            s.cleanupStyles = function () {
                // Container
                s.container.removeClass(s.classNames.join(' ')).removeAttr('style');

                // Wrapper
                s.wrapper.removeAttr('style');

                // Slides
                if (s.slides && s.slides.length) {
                    s.slides
                        .removeClass([
                            s.params.slideVisibleClass,
                            s.params.slideActiveClass,
                            s.params.slideNextClass,
                            s.params.slidePrevClass
                        ].join(' '))
                        .removeAttr('style')
                        .removeAttr('data-swiper-column')
                        .removeAttr('data-swiper-row');
                }

                // Pagination/Bullets
                if (s.paginationContainer && s.paginationContainer.length) {
                    s.paginationContainer.removeClass(s.params.paginationHiddenClass);
                }
                if (s.bullets && s.bullets.length) {
                    s.bullets.removeClass(s.params.bulletActiveClass);
                }

                // Buttons
                if (s.params.prevButton) $(s.params.prevButton).removeClass(s.params.buttonDisabledClass);
                if (s.params.nextButton) $(s.params.nextButton).removeClass(s.params.buttonDisabledClass);

                // Scrollbar
                if (s.params.scrollbar && s.scrollbar) {
                    if (s.scrollbar.track && s.scrollbar.track.length) s.scrollbar.track.removeAttr('style');
                    if (s.scrollbar.drag && s.scrollbar.drag.length) s.scrollbar.drag.removeAttr('style');
                }
            };

            // Destroy
            s.destroy = function (deleteInstance, cleanupStyles) {
                // Detach evebts
                s.detachEvents();
                // Stop autoplay
                s.stopAutoplay();
                // Disable draggable
                if (s.params.scrollbar && s.scrollbar) {
                    if (s.params.scrollbarDraggable) {
                        s.scrollbar.disableDraggable();
                    }
                }
                // Destroy loop
                if (s.params.loop) {
                    s.destroyLoop();
                }
                // Cleanup styles
                if (cleanupStyles) {
                    s.cleanupStyles();
                }
                // Disconnect observer
                s.disconnectObservers();
                // Disable keyboard/mousewheel
                if (s.params.keyboardControl) {
                    if (s.disableKeyboardControl) s.disableKeyboardControl();
                }
                if (s.params.mousewheelControl) {
                    if (s.disableMousewheelControl) s.disableMousewheelControl();
                }
                // Disable a11y
                if (s.params.a11y && s.a11y) s.a11y.destroy();
                // Destroy callback
                s.emit('onDestroy');
                // Delete instance
                if (deleteInstance !== false) s = null;
            };

            s.init();


            // Return swiper instance
            return s;
        };


        /*==================================================
         Prototype
         ====================================================*/
        Swiper.prototype = {
            isSafari: (function () {
                var ua = navigator.userAgent.toLowerCase();
                return (ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0 && ua.indexOf('android') < 0);
            })(),
            isUiWebView: /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(navigator.userAgent),
            isArray: function (arr) {
                return Object.prototype.toString.apply(arr) === '[object Array]';
            },
            /*==================================================
             Browser
             ====================================================*/
            browser: {
                ie: window.navigator.pointerEnabled || window.navigator.msPointerEnabled,
                ieTouch: (window.navigator.msPointerEnabled && window.navigator.msMaxTouchPoints > 1) || (window.navigator.pointerEnabled && window.navigator.maxTouchPoints > 1)
            },
            /*==================================================
             Devices
             ====================================================*/
            device: (function () {
                var ua = navigator.userAgent;
                var android = ua.match(/(Android);?[\s\/]+([\d.]+)?/);
                var ipad = ua.match(/(iPad).*OS\s([\d_]+)/);
                var ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/);
                var iphone = !ipad && ua.match(/(iPhone\sOS)\s([\d_]+)/);
                return {
                    ios: ipad || iphone || ipod,
                    android: android
                };
            })(),
            /*==================================================
             Feature Detection
             ====================================================*/
            support: {
                touch: (window.Modernizr && Modernizr.touch === true) || (function () {
                    return !!(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch);
                })(),

                transforms3d: (window.Modernizr && Modernizr.csstransforms3d === true) || (function () {
                    var div = document.createElement('div').style;
                    return ('webkitPerspective' in div || 'MozPerspective' in div || 'OPerspective' in div || 'MsPerspective' in div || 'perspective' in div);
                })(),

                flexbox: (function () {
                    var div = document.createElement('div').style;
                    var styles = ('alignItems webkitAlignItems webkitBoxAlign msFlexAlign mozBoxAlign webkitFlexDirection msFlexDirection mozBoxDirection mozBoxOrient webkitBoxDirection webkitBoxOrient').split(' ');
                    for (var i = 0; i < styles.length; i++) {
                        if (styles[i] in div) return true;
                    }
                })(),

                observer: (function () {
                    return ('MutationObserver' in window || 'WebkitMutationObserver' in window);
                })()
            },
            /*==================================================
             Plugins
             ====================================================*/
            plugins: {}
        };


        /*===========================
         Dom7 Library
         ===========================*/
        var Dom7 = (function () {
            var Dom7 = function (arr) {
                var _this = this, i = 0;
                // Create array-like object
                for (i = 0; i < arr.length; i++) {
                    _this[i] = arr[i];
                }
                _this.length = arr.length;
                // Return collection with methods
                return this;
            };
            var $ = function (selector, context) {
                var arr = [], i = 0;
                if (selector && !context) {
                    if (selector instanceof Dom7) {
                        return selector;
                    }
                }
                if (selector) {
                    // String
                    if (typeof selector === 'string') {
                        var els, tempParent, html = selector.trim();
                        if (html.indexOf('<') >= 0 && html.indexOf('>') >= 0) {
                            var toCreate = 'div';
                            if (html.indexOf('<li') === 0) toCreate = 'ul';
                            if (html.indexOf('<tr') === 0) toCreate = 'tbody';
                            if (html.indexOf('<td') === 0 || html.indexOf('<th') === 0) toCreate = 'tr';
                            if (html.indexOf('<tbody') === 0) toCreate = 'table';
                            if (html.indexOf('<option') === 0) toCreate = 'select';
                            tempParent = document.createElement(toCreate);
                            tempParent.innerHTML = selector;
                            for (i = 0; i < tempParent.childNodes.length; i++) {
                                arr.push(tempParent.childNodes[i]);
                            }
                        }
                        else {
                            if (!context && selector[0] === '#' && !selector.match(/[ .<>:~]/)) {
                                // Pure ID selector
                                els = [document.getElementById(selector.split('#')[1])];
                            }
                            else {
                                // Other selectors
                                els = (context || document).querySelectorAll(selector);
                            }
                            for (i = 0; i < els.length; i++) {
                                if (els[i]) arr.push(els[i]);
                            }
                        }
                    }
                    // Node/element
                    else if (selector.nodeType || selector === window || selector === document) {
                        arr.push(selector);
                    }
                    //Array of elements or instance of Dom
                    else if (selector.length > 0 && selector[0].nodeType) {
                        for (i = 0; i < selector.length; i++) {
                            arr.push(selector[i]);
                        }
                    }
                }
                return new Dom7(arr);
            };
            Dom7.prototype = {
                // Classes and attriutes
                addClass: function (className) {
                    if (typeof className === 'undefined') {
                        return this;
                    }
                    var classes = className.split(' ');
                    for (var i = 0; i < classes.length; i++) {
                        for (var j = 0; j < this.length; j++) {
                            this[j].classList.add(classes[i]);
                        }
                    }
                    return this;
                },
                removeClass: function (className) {
                    var classes = className.split(' ');
                    for (var i = 0; i < classes.length; i++) {
                        for (var j = 0; j < this.length; j++) {
                            this[j].classList.remove(classes[i]);
                        }
                    }
                    return this;
                },
                hasClass: function (className) {
                    if (!this[0]) return false;
                    else return this[0].classList.contains(className);
                },
                toggleClass: function (className) {
                    var classes = className.split(' ');
                    for (var i = 0; i < classes.length; i++) {
                        for (var j = 0; j < this.length; j++) {
                            this[j].classList.toggle(classes[i]);
                        }
                    }
                    return this;
                },
                attr: function (attrs, value) {
                    if (arguments.length === 1 && typeof attrs === 'string') {
                        // Get attr
                        if (this[0]) return this[0].getAttribute(attrs);
                        else return undefined;
                    }
                    else {
                        // Set attrs
                        for (var i = 0; i < this.length; i++) {
                            if (arguments.length === 2) {
                                // String
                                this[i].setAttribute(attrs, value);
                            }
                            else {
                                // Object
                                for (var attrName in attrs) {
                                    this[i][attrName] = attrs[attrName];
                                    this[i].setAttribute(attrName, attrs[attrName]);
                                }
                            }
                        }
                        return this;
                    }
                },
                removeAttr: function (attr) {
                    for (var i = 0; i < this.length; i++) {
                        this[i].removeAttribute(attr);
                    }
                    return this;
                },
                data: function (key, value) {
                    if (typeof value === 'undefined') {
                        // Get value
                        if (this[0]) {
                            var dataKey = this[0].getAttribute('data-' + key);
                            if (dataKey) return dataKey;
                            else if (this[0].dom7ElementDataStorage && (key in this[0].dom7ElementDataStorage)) return this[0].dom7ElementDataStorage[key];
                            else return undefined;
                        }
                        else return undefined;
                    }
                    else {
                        // Set value
                        for (var i = 0; i < this.length; i++) {
                            var el = this[i];
                            if (!el.dom7ElementDataStorage) el.dom7ElementDataStorage = {};
                            el.dom7ElementDataStorage[key] = value;
                        }
                        return this;
                    }
                },
                // Transforms
                transform: function (transform) {
                    for (var i = 0; i < this.length; i++) {
                        var elStyle = this[i].style;
                        elStyle.webkitTransform = elStyle.MsTransform = elStyle.msTransform = elStyle.MozTransform = elStyle.OTransform = elStyle.transform = transform;
                    }
                    return this;
                },
                transition: function (duration) {
                    if (typeof duration !== 'string') {
                        duration = duration + 'ms';
                    }
                    for (var i = 0; i < this.length; i++) {
                        var elStyle = this[i].style;
                        elStyle.webkitTransitionDuration = elStyle.MsTransitionDuration = elStyle.msTransitionDuration = elStyle.MozTransitionDuration = elStyle.OTransitionDuration = elStyle.transitionDuration = duration;
                    }
                    return this;
                },
                //Events
                on: function (eventName, targetSelector, listener, capture) {
                    function handleLiveEvent(e) {
                        var target = e.target;
                        if ($(target).is(targetSelector)) listener.call(target, e);
                        else {
                            var parents = $(target).parents();
                            for (var k = 0; k < parents.length; k++) {
                                if ($(parents[k]).is(targetSelector)) listener.call(parents[k], e);
                            }
                        }
                    }

                    var events = eventName.split(' ');
                    var i, j;
                    for (i = 0; i < this.length; i++) {
                        if (typeof targetSelector === 'function' || targetSelector === false) {
                            // Usual events
                            if (typeof targetSelector === 'function') {
                                listener = arguments[1];
                                capture = arguments[2] || false;
                            }
                            for (j = 0; j < events.length; j++) {
                                this[i].addEventListener(events[j], listener, capture);
                            }
                        }
                        else {
                            //Live events
                            for (j = 0; j < events.length; j++) {
                                if (!this[i].dom7LiveListeners) this[i].dom7LiveListeners = [];
                                this[i].dom7LiveListeners.push({listener: listener, liveListener: handleLiveEvent});
                                this[i].addEventListener(events[j], handleLiveEvent, capture);
                            }
                        }
                    }

                    return this;
                },
                off: function (eventName, targetSelector, listener, capture) {
                    var events = eventName.split(' ');
                    for (var i = 0; i < events.length; i++) {
                        for (var j = 0; j < this.length; j++) {
                            if (typeof targetSelector === 'function' || targetSelector === false) {
                                // Usual events
                                if (typeof targetSelector === 'function') {
                                    listener = arguments[1];
                                    capture = arguments[2] || false;
                                }
                                this[j].removeEventListener(events[i], listener, capture);
                            }
                            else {
                                // Live event
                                if (this[j].dom7LiveListeners) {
                                    for (var k = 0; k < this[j].dom7LiveListeners.length; k++) {
                                        if (this[j].dom7LiveListeners[k].listener === listener) {
                                            this[j].removeEventListener(events[i], this[j].dom7LiveListeners[k].liveListener, capture);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    return this;
                },
                once: function (eventName, targetSelector, listener, capture) {
                    var dom = this;
                    if (typeof targetSelector === 'function') {
                        targetSelector = false;
                        listener = arguments[1];
                        capture = arguments[2];
                    }
                    function proxy(e) {
                        listener(e);
                        dom.off(eventName, targetSelector, proxy, capture);
                    }

                    dom.on(eventName, targetSelector, proxy, capture);
                },
                trigger: function (eventName, eventData) {
                    for (var i = 0; i < this.length; i++) {
                        var evt;
                        try {
                            evt = new window.CustomEvent(eventName, {
                                detail: eventData,
                                bubbles: true,
                                cancelable: true
                            });
                        }
                        catch (e) {
                            evt = document.createEvent('Event');
                            evt.initEvent(eventName, true, true);
                            evt.detail = eventData;
                        }
                        this[i].dispatchEvent(evt);
                    }
                    return this;
                },
                transitionEnd: function (callback) {
                    var events = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd'],
                        i, j, dom = this;

                    function fireCallBack(e) {
                        /*jshint validthis:true */
                        if (e.target !== this) return;
                        callback.call(this, e);
                        for (i = 0; i < events.length; i++) {
                            dom.off(events[i], fireCallBack);
                        }
                    }

                    if (callback) {
                        for (i = 0; i < events.length; i++) {
                            dom.on(events[i], fireCallBack);
                        }
                    }
                    return this;
                },
                // Sizing/Styles
                width: function () {
                    if (this[0] === window) {
                        return window.innerWidth;
                    }
                    else {
                        if (this.length > 0) {
                            return parseFloat(this.css('width'));
                        }
                        else {
                            return null;
                        }
                    }
                },
                outerWidth: function (includeMargins) {
                    if (this.length > 0) {
                        if (includeMargins)
                            return this[0].offsetWidth + parseFloat(this.css('margin-right')) + parseFloat(this.css('margin-left'));
                        else
                            return this[0].offsetWidth;
                    }
                    else return null;
                },
                height: function () {
                    if (this[0] === window) {
                        return window.innerHeight;
                    }
                    else {
                        if (this.length > 0) {
                            return parseFloat(this.css('height'));
                        }
                        else {
                            return null;
                        }
                    }
                },
                outerHeight: function (includeMargins) {
                    if (this.length > 0) {
                        if (includeMargins)
                            return this[0].offsetHeight + parseFloat(this.css('margin-top')) + parseFloat(this.css('margin-bottom'));
                        else
                            return this[0].offsetHeight;
                    }
                    else return null;
                },
                offset: function () {
                    if (this.length > 0) {
                        var el = this[0];
                        var box = el.getBoundingClientRect();
                        var body = document.body;
                        var clientTop = el.clientTop || body.clientTop || 0;
                        var clientLeft = el.clientLeft || body.clientLeft || 0;
                        var scrollTop = window.pageYOffset || el.scrollTop;
                        var scrollLeft = window.pageXOffset || el.scrollLeft;
                        return {
                            top: box.top + scrollTop - clientTop,
                            left: box.left + scrollLeft - clientLeft
                        };
                    }
                    else {
                        return null;
                    }
                },
                css: function (props, value) {
                    var i;
                    if (arguments.length === 1) {
                        if (typeof props === 'string') {
                            if (this[0]) return window.getComputedStyle(this[0], null).getPropertyValue(props);
                        }
                        else {
                            for (i = 0; i < this.length; i++) {
                                for (var prop in props) {
                                    this[i].style[prop] = props[prop];
                                }
                            }
                            return this;
                        }
                    }
                    if (arguments.length === 2 && typeof props === 'string') {
                        for (i = 0; i < this.length; i++) {
                            this[i].style[props] = value;
                        }
                        return this;
                    }
                    return this;
                },

                //Dom manipulation
                each: function (callback) {
                    for (var i = 0; i < this.length; i++) {
                        callback.call(this[i], i, this[i]);
                    }
                    return this;
                },
                html: function (html) {
                    if (typeof html === 'undefined') {
                        return this[0] ? this[0].innerHTML : undefined;
                    }
                    else {
                        for (var i = 0; i < this.length; i++) {
                            this[i].innerHTML = html;
                        }
                        return this;
                    }
                },
                is: function (selector) {
                    if (!this[0]) return false;
                    var compareWith, i;
                    if (typeof selector === 'string') {
                        var el = this[0];
                        if (el === document) return selector === document;
                        if (el === window) return selector === window;

                        if (el.matches) return el.matches(selector);
                        else if (el.webkitMatchesSelector) return el.webkitMatchesSelector(selector);
                        else if (el.mozMatchesSelector) return el.mozMatchesSelector(selector);
                        else if (el.msMatchesSelector) return el.msMatchesSelector(selector);
                        else {
                            compareWith = $(selector);
                            for (i = 0; i < compareWith.length; i++) {
                                if (compareWith[i] === this[0]) return true;
                            }
                            return false;
                        }
                    }
                    else if (selector === document) return this[0] === document;
                    else if (selector === window) return this[0] === window;
                    else {
                        if (selector.nodeType || selector instanceof Dom7) {
                            compareWith = selector.nodeType ? [selector] : selector;
                            for (i = 0; i < compareWith.length; i++) {
                                if (compareWith[i] === this[0]) return true;
                            }
                            return false;
                        }
                        return false;
                    }

                },
                index: function () {
                    if (this[0]) {
                        var child = this[0];
                        var i = 0;
                        while ((child = child.previousSibling) !== null) {
                            if (child.nodeType === 1) i++;
                        }
                        return i;
                    }
                    else return undefined;
                },
                eq: function (index) {
                    if (typeof index === 'undefined') return this;
                    var length = this.length;
                    var returnIndex;
                    if (index > length - 1) {
                        return new Dom7([]);
                    }
                    if (index < 0) {
                        returnIndex = length + index;
                        if (returnIndex < 0) return new Dom7([]);
                        else return new Dom7([this[returnIndex]]);
                    }
                    return new Dom7([this[index]]);
                },
                append: function (newChild) {
                    var i, j;
                    for (i = 0; i < this.length; i++) {
                        if (typeof newChild === 'string') {
                            var tempDiv = document.createElement('div');
                            tempDiv.innerHTML = newChild;
                            while (tempDiv.firstChild) {
                                this[i].appendChild(tempDiv.firstChild);
                            }
                        }
                        else if (newChild instanceof Dom7) {
                            for (j = 0; j < newChild.length; j++) {
                                this[i].appendChild(newChild[j]);
                            }
                        }
                        else {
                            this[i].appendChild(newChild);
                        }
                    }
                    return this;
                },
                prepend: function (newChild) {
                    var i, j;
                    for (i = 0; i < this.length; i++) {
                        if (typeof newChild === 'string') {
                            var tempDiv = document.createElement('div');
                            tempDiv.innerHTML = newChild;
                            for (j = tempDiv.childNodes.length - 1; j >= 0; j--) {
                                this[i].insertBefore(tempDiv.childNodes[j], this[i].childNodes[0]);
                            }
                            // this[i].insertAdjacentHTML('afterbegin', newChild);
                        }
                        else if (newChild instanceof Dom7) {
                            for (j = 0; j < newChild.length; j++) {
                                this[i].insertBefore(newChild[j], this[i].childNodes[0]);
                            }
                        }
                        else {
                            this[i].insertBefore(newChild, this[i].childNodes[0]);
                        }
                    }
                    return this;
                },
                insertBefore: function (selector) {
                    var before = $(selector);
                    for (var i = 0; i < this.length; i++) {
                        if (before.length === 1) {
                            before[0].parentNode.insertBefore(this[i], before[0]);
                        }
                        else if (before.length > 1) {
                            for (var j = 0; j < before.length; j++) {
                                before[j].parentNode.insertBefore(this[i].cloneNode(true), before[j]);
                            }
                        }
                    }
                },
                insertAfter: function (selector) {
                    var after = $(selector);
                    for (var i = 0; i < this.length; i++) {
                        if (after.length === 1) {
                            after[0].parentNode.insertBefore(this[i], after[0].nextSibling);
                        }
                        else if (after.length > 1) {
                            for (var j = 0; j < after.length; j++) {
                                after[j].parentNode.insertBefore(this[i].cloneNode(true), after[j].nextSibling);
                            }
                        }
                    }
                },
                next: function (selector) {
                    if (this.length > 0) {
                        if (selector) {
                            if (this[0].nextElementSibling && $(this[0].nextElementSibling).is(selector)) return new Dom7([this[0].nextElementSibling]);
                            else return new Dom7([]);
                        }
                        else {
                            if (this[0].nextElementSibling) return new Dom7([this[0].nextElementSibling]);
                            else return new Dom7([]);
                        }
                    }
                    else return new Dom7([]);
                },
                nextAll: function (selector) {
                    var nextEls = [];
                    var el = this[0];
                    if (!el) return new Dom7([]);
                    while (el.nextElementSibling) {
                        var next = el.nextElementSibling;
                        if (selector) {
                            if ($(next).is(selector)) nextEls.push(next);
                        }
                        else nextEls.push(next);
                        el = next;
                    }
                    return new Dom7(nextEls);
                },
                prev: function (selector) {
                    if (this.length > 0) {
                        if (selector) {
                            if (this[0].previousElementSibling && $(this[0].previousElementSibling).is(selector)) return new Dom7([this[0].previousElementSibling]);
                            else return new Dom7([]);
                        }
                        else {
                            if (this[0].previousElementSibling) return new Dom7([this[0].previousElementSibling]);
                            else return new Dom7([]);
                        }
                    }
                    else return new Dom7([]);
                },
                prevAll: function (selector) {
                    var prevEls = [];
                    var el = this[0];
                    if (!el) return new Dom7([]);
                    while (el.previousElementSibling) {
                        var prev = el.previousElementSibling;
                        if (selector) {
                            if ($(prev).is(selector)) prevEls.push(prev);
                        }
                        else prevEls.push(prev);
                        el = prev;
                    }
                    return new Dom7(prevEls);
                },
                parent: function (selector) {
                    var parents = [];
                    for (var i = 0; i < this.length; i++) {
                        if (selector) {
                            if ($(this[i].parentNode).is(selector)) parents.push(this[i].parentNode);
                        }
                        else {
                            parents.push(this[i].parentNode);
                        }
                    }
                    return $($.unique(parents));
                },
                parents: function (selector) {
                    var parents = [];
                    for (var i = 0; i < this.length; i++) {
                        var parent = this[i].parentNode;
                        while (parent) {
                            if (selector) {
                                if ($(parent).is(selector)) parents.push(parent);
                            }
                            else {
                                parents.push(parent);
                            }
                            parent = parent.parentNode;
                        }
                    }
                    return $($.unique(parents));
                },
                find: function (selector) {
                    var foundElements = [];
                    for (var i = 0; i < this.length; i++) {
                        var found = this[i].querySelectorAll(selector);
                        for (var j = 0; j < found.length; j++) {
                            foundElements.push(found[j]);
                        }
                    }
                    return new Dom7(foundElements);
                },
                children: function (selector) {
                    var children = [];
                    for (var i = 0; i < this.length; i++) {
                        var childNodes = this[i].childNodes;

                        for (var j = 0; j < childNodes.length; j++) {
                            if (!selector) {
                                if (childNodes[j].nodeType === 1) children.push(childNodes[j]);
                            }
                            else {
                                if (childNodes[j].nodeType === 1 && $(childNodes[j]).is(selector)) children.push(childNodes[j]);
                            }
                        }
                    }
                    return new Dom7($.unique(children));
                },
                remove: function () {
                    for (var i = 0; i < this.length; i++) {
                        if (this[i].parentNode) this[i].parentNode.removeChild(this[i]);
                    }
                    return this;
                },
                add: function () {
                    var dom = this;
                    var i, j;
                    for (i = 0; i < arguments.length; i++) {
                        var toAdd = $(arguments[i]);
                        for (j = 0; j < toAdd.length; j++) {
                            dom[dom.length] = toAdd[j];
                            dom.length++;
                        }
                    }
                    return dom;
                }
            };
            $.fn = Dom7.prototype;
            $.unique = function (arr) {
                var unique = [];
                for (var i = 0; i < arr.length; i++) {
                    if (unique.indexOf(arr[i]) === -1) unique.push(arr[i]);
                }
                return unique;
            };

            return $;
        })();


        /*===========================
         Get Dom libraries
         ===========================*/
        var swiperDomPlugins = ['jQuery', 'Zepto', 'Dom7'];
        for (var i = 0; i < swiperDomPlugins.length; i++) {
            if (window[swiperDomPlugins[i]]) {
                addLibraryPlugin(window[swiperDomPlugins[i]]);
            }
        }
        // Required DOM Plugins
        var domLib;
        if (typeof Dom7 === 'undefined') {
            domLib = window.Dom7 || window.Zepto || window.jQuery;
        }
        else {
            domLib = Dom7;
        }

        /*===========================
         Add .swiper plugin from Dom libraries
         ===========================*/
        function addLibraryPlugin(lib) {
            lib.fn.swiper = function (params) {
                var firstInstance;
                lib(this).each(function () {
                    var s = new Swiper(this, params);
                    if (!firstInstance) firstInstance = s;
                });
                return firstInstance;
            };
        }

        if (domLib) {
            if (!('transitionEnd' in domLib.fn)) {
                domLib.fn.transitionEnd = function (callback) {
                    var events = ['webkitTransitionEnd', 'transitionend', 'oTransitionEnd', 'MSTransitionEnd', 'msTransitionEnd'],
                        i, j, dom = this;

                    function fireCallBack(e) {
                        /*jshint validthis:true */
                        if (e.target !== this) return;
                        callback.call(this, e);
                        for (i = 0; i < events.length; i++) {
                            dom.off(events[i], fireCallBack);
                        }
                    }

                    if (callback) {
                        for (i = 0; i < events.length; i++) {
                            dom.on(events[i], fireCallBack);
                        }
                    }
                    return this;
                };
            }
            if (!('transform' in domLib.fn)) {
                domLib.fn.transform = function (transform) {
                    for (var i = 0; i < this.length; i++) {
                        var elStyle = this[i].style;
                        elStyle.webkitTransform = elStyle.MsTransform = elStyle.msTransform = elStyle.MozTransform = elStyle.OTransform = elStyle.transform = transform;
                    }
                    return this;
                };
            }
            if (!('transition' in domLib.fn)) {
                domLib.fn.transition = function (duration) {
                    if (typeof duration !== 'string') {
                        duration = duration + 'ms';
                    }
                    for (var i = 0; i < this.length; i++) {
                        var elStyle = this[i].style;
                        elStyle.webkitTransitionDuration = elStyle.MsTransitionDuration = elStyle.msTransitionDuration = elStyle.MozTransitionDuration = elStyle.OTransitionDuration = elStyle.transitionDuration = duration;
                    }
                    return this;
                };
            }
        }

        ionic.views.Swiper = Swiper;
    })();

    (function (ionic) {
        'use strict';

        ionic.views.Toggle = ionic.views.View.inherit({
            initialize: function (opts) {
                var self = this;

                this.el = opts.el;
                this.checkbox = opts.checkbox;
                this.track = opts.track;
                this.handle = opts.handle;
                this.openPercent = -1;
                this.onChange = opts.onChange || function () {
                    };

                this.triggerThreshold = opts.triggerThreshold || 20;

                this.dragStartHandler = function (e) {
                    self.dragStart(e);
                };
                this.dragHandler = function (e) {
                    self.drag(e);
                };
                this.holdHandler = function (e) {
                    self.hold(e);
                };
                this.releaseHandler = function (e) {
                    self.release(e);
                };

                this.dragStartGesture = ionic.onGesture('dragstart', this.dragStartHandler, this.el);
                this.dragGesture = ionic.onGesture('drag', this.dragHandler, this.el);
                this.dragHoldGesture = ionic.onGesture('hold', this.holdHandler, this.el);
                this.dragReleaseGesture = ionic.onGesture('release', this.releaseHandler, this.el);
            },

            destroy: function () {
                ionic.offGesture(this.dragStartGesture, 'dragstart', this.dragStartGesture);
                ionic.offGesture(this.dragGesture, 'drag', this.dragGesture);
                ionic.offGesture(this.dragHoldGesture, 'hold', this.holdHandler);
                ionic.offGesture(this.dragReleaseGesture, 'release', this.releaseHandler);
            },

            tap: function () {
                if (this.el.getAttribute('disabled') !== 'disabled') {
                    this.val(!this.checkbox.checked);
                }
            },

            dragStart: function (e) {
                if (this.checkbox.disabled) return;

                this._dragInfo = {
                    width: this.el.offsetWidth,
                    left: this.el.offsetLeft,
                    right: this.el.offsetLeft + this.el.offsetWidth,
                    triggerX: this.el.offsetWidth / 2,
                    initialState: this.checkbox.checked
                };

                // Stop any parent dragging
                e.gesture.srcEvent.preventDefault();

                // Trigger hold styles
                this.hold(e);
            },

            drag: function (e) {
                var self = this;
                if (!this._dragInfo) {
                    return;
                }

                // Stop any parent dragging
                e.gesture.srcEvent.preventDefault();

                ionic.requestAnimationFrame(function () {
                    if (!self._dragInfo) {
                        return;
                    }

                    var px = e.gesture.touches[0].pageX - self._dragInfo.left;
                    var mx = self._dragInfo.width - self.triggerThreshold;

                    // The initial state was on, so "tend towards" on
                    if (self._dragInfo.initialState) {
                        if (px < self.triggerThreshold) {
                            self.setOpenPercent(0);
                        } else if (px > self._dragInfo.triggerX) {
                            self.setOpenPercent(100);
                        }
                    } else {
                        // The initial state was off, so "tend towards" off
                        if (px < self._dragInfo.triggerX) {
                            self.setOpenPercent(0);
                        } else if (px > mx) {
                            self.setOpenPercent(100);
                        }
                    }
                });
            },

            endDrag: function () {
                this._dragInfo = null;
            },

            hold: function () {
                this.el.classList.add('dragging');
            },
            release: function (e) {
                this.el.classList.remove('dragging');
                this.endDrag(e);
            },


            setOpenPercent: function (openPercent) {
                // only make a change if the new open percent has changed
                if (this.openPercent < 0 || (openPercent < (this.openPercent - 3) || openPercent > (this.openPercent + 3) )) {
                    this.openPercent = openPercent;

                    if (openPercent === 0) {
                        this.val(false);
                    } else if (openPercent === 100) {
                        this.val(true);
                    } else {
                        var openPixel = Math.round((openPercent / 100) * this.track.offsetWidth - (this.handle.offsetWidth));
                        openPixel = (openPixel < 1 ? 0 : openPixel);
                        this.handle.style[ionic.CSS.TRANSFORM] = 'translate3d(' + openPixel + 'px,0,0)';
                    }
                }
            },

            val: function (value) {
                if (value === true || value === false) {
                    if (this.handle.style[ionic.CSS.TRANSFORM] !== "") {
                        this.handle.style[ionic.CSS.TRANSFORM] = "";
                    }
                    this.checkbox.checked = value;
                    this.openPercent = (value ? 100 : 0);
                    this.onChange && this.onChange();
                }
                return this.checkbox.checked;
            }

        });

    })(ionic);

})();